﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Shapes;
using DriveBeamEditor.Controls.BeamNG.Controls;
using HelixToolkit.Wpf;
using JBeamLibrary;
using JBeamLibrary.Tools;
using JBeamLibrary.VehicleParts;
using System.Windows.Threading;
using Petzold.Media3D;
using Brush = System.Windows.Media.Brush;
using Brushes = System.Windows.Media.Brushes;
using Color = System.Windows.Media.Color;
using Point = System.Windows.Point;

namespace DriveBeamEditor.Wpf
{
    /// <summary>
    /// Interaction logic for NodeBeam3DEditor.xaml
    /// </summary>
    public partial class NodeBeam3DEditor : INotifyPropertyChanged
    {
        #region Fields

        private double _snapValue = 1;
        private double _gridSize = 100;
        private string _currentCursor = "None";
        private int _selectionCount = 0;
        private bool _isSelectionActive = false;
        private Point3D _currentPosition;
        private Point3D _realPosition;
        private Model3D _currentModel;
        private Point3D? _lineStart;
        private Point3D? _cursorPoint;
        private Point3D? _movingNode;
        private Point3D? _beamGhost;
        private Dispatcher _dispatcher;
        private Point3D? _triPoint1;
        private Point3D? _triPoint2;
        private int _triIndex1;
        private int _triIndex2;
        private int _triIndex3;
        private Transform3D _modelTransform;
        private bool _movingGrid = false;
        private PipeVisual3D _curThrusterPerspective;
        private PipeVisual3D _curThrusterTop;
        private PipeVisual3D _curThrusterFront;
        private PipeVisual3D _curThrusterRight;
        private TruncatedConeVisual3D _curCameraPerspective;
        private TruncatedConeVisual3D _curCameraTop;
        private TruncatedConeVisual3D _curCameraFront;
        private TruncatedConeVisual3D _curCameraRight;
        private Vector3D _gridLengthVector = new Vector3D(1, 0, 0);
        private Vector3D _gridWidthVector = new Vector3D(0, 1, 0);
        private Vector3D _gridUpVector = new Vector3D(0, 0, 1);
        private Matrix3D _gridTranslation = Matrix3D.Identity;
        private Matrix3D _gridRotation = Matrix3D.Identity;
        private List<int> _selectionNodeIndices = new List<int>();
        private Point3DCollection _originalNodes = new Point3DCollection();
        private Dictionary<string, Model3D> _models = new Dictionary<string, Model3D>();

        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler<Point3D> NodeAdded;
        public event EventHandler<Point3D> NodeRemoved;
        public event EventHandler<Tuple<Point3D, Point3D>> NodeMoved;
        public event EventHandler<List<Tuple<Point3D, Point3D>>> NodesMoved;
        public event EventHandler<Tuple<Point3D, Point3D>> BeamAdded;
        public event EventHandler BeamsAdded;
        public event EventHandler<Tuple<Point3D, Point3D>> BeamRemoved;
        public event EventHandler<Point3D> BeamsRemoved;
        public event EventHandler<Tuple<Point3D, Point3D>> ThrusterAdded;
        public event EventHandler<string> PartSelected;
        public event EventHandler<Tuple<Point3D, Point3D, Point3D>> TriangleAdded;
        public event EventHandler<Tuple<Point3D, Point3D, Point3D>> TriangleRemoved;
        public event EventHandler<Point3D> TrianglesRemoved;
        public event EventHandler<Tuple<Point3D, float>> ExternalCameraChanged;
        public event EventHandler<Tuple<Point3D, float, Tuple<Point3D, Point3D, Point3D, Point3D, Point3D, Point3D>>> InternalCameraAdded;
        public event EventHandler QueryTriangles;
        public event EventHandler QueryReload;
        public event EventHandler QueryLoadModel;
        public event EventHandler<Point3D> QueryNodeInfo;
        public event EventHandler<Tuple<Point3D, Point3D>> QueryBeamsInfo;
        public event EventHandler<Tuple<Point3D, Point3D, Point3D>> QueryTriangleInfo;

        #endregion

        #region Properties

        public Point3D CurrentPosition
        {
            get { return _currentPosition; }
            set
            {
                _currentPosition = value;

                var cursorPos = CursorPosition;
                RealPosition = new Point3D(cursorPos.X / Scale3D, cursorPos.Y / Scale3D, cursorPos.Z / Scale3D);

                OnPropertyChanged();
            }
        }

        public Point3D RealPosition
        {
            get { return _realPosition; }
            set
            {
                _realPosition = value;
                OnPropertyChanged();
            }
        }

        public Model3D CurrentModel
        {
            get { return _currentModel; }
            set
            {
                _currentModel = value;
                Bind3DModel();
                OnPropertyChanged();
            }
        }

        public double SnapValue
        {
            get { return _snapValue; }
            set
            {
                _snapValue = value;
                OnPropertyChanged();
            }
        }

        public double GridSize
        {
            get { return _gridSize; }
            set
            {
                _gridSize = value;
                OnPropertyChanged();
            }
        }

        public Point3D CursorPosition
        {
            get { return SnapPoint(CurrentPosition, SnapValue, PerspectiveGrid.Center); }
        }

        public bool PrefixNodes
        {
            get { return CheckUseNodePrefix.IsChecked.HasValue && CheckUseNodePrefix.IsChecked.Value; }
        }

        public string NodePrefix
        {
            get { return TextNodePrefix.Text; }
        }

        public float Scale3D
        {
            get
            {
                return float.Parse(Config.Map.Get("3DScale", "10"));
            }
            set
            {
                Config.Map.Set("3DScale", value.ToStringOrDefault("10"));
                OnPropertyChanged();
            }
        }

        public float ModelScale
        {
            get { return float.Parse(Config.Map.Get("3DModelScale", "1")); }
            set
            {
                Config.Map.Set("3DModelScale", value.ToStringOrDefault("1"));
                OnPropertyChanged();
            }
        }

        public float ModelManipulatorSize
        {
            get { return float.Parse(Config.Map.Get("3DModelManipulatorSize", "5")); }
            set
            {
                Config.Map.Set("3DModelManipulatorSize", value.ToStringOrDefault("5"));
                OnPropertyChanged();
            }
        }

        public Transform3D ModelTransform
        {
            get { return _modelTransform; }
            set
            {
                _modelTransform = value;
                OnPropertyChanged();
            }
        }

        public byte TriangleOpacity
        {
            get { return ((SolidColorBrush)Resources["TriangleBrush"]).Color.A; }
            set
            {
                var brush = (SolidColorBrush)Resources["TriangleBrush"];
                var color = Color.FromArgb(value, brush.Color.R, brush.Color.G, brush.Color.B);
                Resources["TriangleBrush"] = new SolidColorBrush(color);
            }
        }

        public int SelectionCount
        {
            get { return _selectionCount; }
            set
            {
                _selectionCount = value;
                OnPropertyChanged();
            }
        }

        public bool IsSelectionActive
        {
            get { return _isSelectionActive; }
            set
            {
                _isSelectionActive = value;
                OnPropertyChanged();
            }
        }

        public string CurrentCursor
        {
            get
            {
                //if (Check3DCursor.IsChecked == true)
                //    return "3D";
                //if (CheckGridCursor.IsChecked == true)
                //    return "Grid";
                //if (CheckNodeCursor.IsChecked == true)
                //    return "Node";
                //return "None";
                return _currentCursor;
            }
            set
            {
                _currentCursor = value;
                OnPropertyChanged();
            }
        }

        #endregion

        #region CTORs

        public NodeBeam3DEditor()
        {
            InitializeComponent();
            DataContext = this;

            ComboNodeColor.ItemsSource = typeof(Colors).GetProperties();
            ComboBeamColor.ItemsSource = typeof(Colors).GetProperties();
            ComboTriangleColor.ItemsSource = typeof(Brushes).GetProperties();
            ComboThrusterColor.ItemsSource = typeof(Brushes).GetProperties();
            ComboCameraColor.ItemsSource = typeof(Brushes).GetProperties();
            ComboGhostColor.ItemsSource = typeof(Colors).GetProperties();
            ComboCursorColor.ItemsSource = typeof(Colors).GetProperties();
            ComboSelectionColor.ItemsSource = typeof(Colors).GetProperties();
            ComboNameColor.ItemsSource = typeof(Brushes).GetProperties();
            ComboPartNodeColor.ItemsSource = typeof(Colors).GetProperties();
            ComboPartBeamColor.ItemsSource = typeof(Colors).GetProperties();

            ComboNodeColor.SelectedItem = typeof(Colors).GetProperty(Config.Map.Get("3DNodeColor", "DarkOrange"));
            ComboBeamColor.SelectedItem = typeof(Colors).GetProperty(Config.Map.Get("3DBeamColor", "Black"));
            ComboTriangleColor.SelectedItem = typeof(Brushes).GetProperty(Config.Map.Get("3DTriangleColor", "ForestGreen"));
            ComboThrusterColor.SelectedItem = typeof(Brushes).GetProperty(Config.Map.Get("3DThrusterColor", "RoyalBlue"));
            ComboCameraColor.SelectedItem = typeof(Brushes).GetProperty(Config.Map.Get("3DCameraColor", "DarkRed"));
            ComboGhostColor.SelectedItem = typeof(Colors).GetProperty(Config.Map.Get("3DGhostColor", "White"));
            ComboCursorColor.SelectedItem = typeof(Colors).GetProperty(Config.Map.Get("3DCursorColor", "RoyalBlue"));
            ComboSelectionColor.SelectedItem = typeof(Colors).GetProperty(Config.Map.Get("3DSelectionColor", "Yellow"));
            ComboNameColor.SelectedItem = typeof(Brushes).GetProperty(Config.Map.Get("3DNameColor", "Yellow"));
            ComboPartNodeColor.SelectedItem = typeof(Colors).GetProperty(Config.Map.Get("3DPartNodeColor", "Black"));
            ComboPartBeamColor.SelectedItem = typeof(Colors).GetProperty(Config.Map.Get("3DPartBeamColor", "Honeydew"));

            _dispatcher = Dispatcher.CurrentDispatcher;

            var modelTopBinding = new Binding("CurrentModel") { Source = this };
            var modelFrontBinding = new Binding("CurrentModel") { Source = this };
            var modelRightBinding = new Binding("CurrentModel") { Source = this };
            var modelPerspectiveBinding = new Binding("CurrentModel") { Source = this };
            BindingOperations.SetBinding(ModelTop, ModelVisual3D.ContentProperty, modelTopBinding);
            BindingOperations.SetBinding(ModelFront, ModelVisual3D.ContentProperty, modelFrontBinding);
            BindingOperations.SetBinding(ModelRight, ModelVisual3D.ContentProperty, modelRightBinding);
            BindingOperations.SetBinding(ModelPerspective, ModelVisual3D.ContentProperty, modelPerspectiveBinding);

            var modelTopTransformBinding = new Binding("Transform") { Source = ModelPerspective };
            var modelFrontTransformBinding = new Binding("Transform") { Source = ModelPerspective };
            var modelRightTransformBinding = new Binding("Transform") { Source = ModelPerspective };
            BindingOperations.SetBinding(ModelTop, ModelVisual3D.TransformProperty, modelTopTransformBinding);
            BindingOperations.SetBinding(ModelFront, ModelVisual3D.TransformProperty, modelFrontTransformBinding);
            BindingOperations.SetBinding(ModelRight, ModelVisual3D.TransformProperty, modelRightTransformBinding);

            ModelTransform = new ScaleTransform3D(Scale3D, Scale3D, Scale3D);
            UpdateLightSphere();

            PerspectiveViewport.Children.Remove(ToolTipPerspective);
            TopViewport.Children.Remove(ToolTipTop);
            FrontViewport.Children.Remove(ToolTipFront);
            RightViewport.Children.Remove(ToolTipRight);
        }

        #endregion

        #region Events

        #region Check buttons

        private void CheckShowMouseCursor_Checked(object sender, RoutedEventArgs e)
        {
            TopViewport.Cursor = Cursor;
            FrontViewport.Cursor = Cursor;
            RightViewport.Cursor = Cursor;
            PerspectiveViewport.Cursor = Cursor;
        }

        private void CheckShowMouseCursor_Unchecked(object sender, RoutedEventArgs e)
        {
            TopViewport.Cursor = Cursors.None;
            FrontViewport.Cursor = Cursors.None;
            RightViewport.Cursor = Cursors.None;
            PerspectiveViewport.Cursor = Cursors.None;
        }

        private void CheckShow3DCursor_OnChecked(object sender, RoutedEventArgs e)
        {
        }

        private void CheckShow3DCursor_OnUnchecked(object sender, RoutedEventArgs e)
        {
        }

        private void CheckNodeCursor_Checked(object sender, RoutedEventArgs e)
        {
            if (Check3DCursor != null)
                Check3DCursor.IsChecked = false;
            if (CheckGridCursor != null)
                CheckGridCursor.IsChecked = false;

            CurrentCursor = "Node";
        }

        private void Check3DCursor_Checked(object sender, RoutedEventArgs e)
        {
            if (CheckNodeCursor != null)
                CheckNodeCursor.IsChecked = false;
            if (CheckGridCursor != null)
                CheckGridCursor.IsChecked = false;

            CurrentCursor = "3D";
        }

        private void CheckGridCursor_Checked(object sender, RoutedEventArgs e)
        {
            if (CheckNodeCursor != null)
                CheckNodeCursor.IsChecked = false;
            if (Check3DCursor != null)
                Check3DCursor.IsChecked = false;

            CurrentCursor = "Grid";
        }

        private void CheckNodeNames_OnChecked(object sender, RoutedEventArgs e)
        {
            if (NodeNamesPerspective != null)
                NodeNamesPerspective.Foreground = (SolidColorBrush)Resources["NodeNameBrush"];
            if (NodeNamesTop != null)
                NodeNamesTop.Foreground = (SolidColorBrush)Resources["NodeNameBrush"];
            if (NodeNamesFront != null)
                NodeNamesFront.Foreground = (SolidColorBrush)Resources["NodeNameBrush"];
            if (NodeNamesRight != null)
                NodeNamesRight.Foreground = (SolidColorBrush)Resources["NodeNameBrush"];
        }

        private void CheckNodeNames_OnUnchecked(object sender, RoutedEventArgs e)
        {
            if (NodeNamesPerspective != null)
                NodeNamesPerspective.Foreground = (SolidColorBrush)Resources["NodeNameBrushHidden"];
            if (NodeNamesTop != null)
                NodeNamesTop.Foreground = (SolidColorBrush)Resources["NodeNameBrushHidden"];
            if (NodeNamesFront != null)
                NodeNamesFront.Foreground = (SolidColorBrush)Resources["NodeNameBrushHidden"];
            if (NodeNamesRight != null)
                NodeNamesRight.Foreground = (SolidColorBrush)Resources["NodeNameBrushHidden"];
        }

        private void CheckBoxPart_Checked(object sender, RoutedEventArgs e)
        {
            if (PartSelected != null)
                PartSelected(sender, (string)((CheckBox)sender).Tag);
        }

        private void CheckBoxPart_Unchecked(object sender, RoutedEventArgs e)
        {
            RemovePart((string)((CheckBox)sender).Tag);
        }

        private void CheckShowTriangles_OnChecked(object sender, RoutedEventArgs e)
        {
            if (QueryTriangles != null)
                QueryTriangles(this, new EventArgs());
        }

        private void CheckShowTriangles_OnUnchecked(object sender, RoutedEventArgs e)
        {
            ClearTriangles();
        }

        private void CheckShowThrusters_Checked(object sender, RoutedEventArgs e)
        {
            SetThrustersVisible(true);
        }

        private void CheckShowThrusters_Unchecked(object sender, RoutedEventArgs e)
        {
            SetThrustersVisible(false);
        }

        private void CheckShowCameras_OnChecked(object sender, RoutedEventArgs e)
        {
            SetCamerasVisible(true);
        }

        private void CheckShowCameras_OnUnchecked(object sender, RoutedEventArgs e)
        {
            SetCamerasVisible(false);
        }

        private void CheckShowNormals_Checked(object sender, RoutedEventArgs e)
        {
            UpdateNormals();
        }

        private void CheckShowNormals_Unchecked(object sender, RoutedEventArgs e)
        {
            ClearNormals();
        }

        private void CheckShowGrid_Checked(object sender, RoutedEventArgs e)
        {
            SetGridVisible(true);
        }

        private void CheckShowGrid_Unchecked(object sender, RoutedEventArgs e)
        {
            SetGridVisible(false);
        }

        private void CheckShowModel_OnChecked(object sender, RoutedEventArgs e)
        {
            ReloadModel();
        }

        private void CheckShowModel_OnUnchecked(object sender, RoutedEventArgs e)
        {
            HideModel();
        }

        private void CheckShowInfo_OnChecked(object sender, RoutedEventArgs e)
        {

        }

        private void CheckShowInfo_OnUnchecked(object sender, RoutedEventArgs e)
        {

        }

        private void CheckAddTriangle_Unchecked(object sender, RoutedEventArgs e)
        {
            _triPoint1 = null;
            _triPoint2 = null;
        }

        private void CheckAutoSelect_OnChecked(object sender, RoutedEventArgs e)
        {

        }

        private void CheckAutoSelect_OnUnchecked(object sender, RoutedEventArgs e)
        {

        }

        #endregion

        #region Buttons

        private void ButtonSelectAllparts_OnClick(object sender, RoutedEventArgs e)
        {
            foreach (var partBox in PartsPanel.Children.OfType<CheckBox>())
                partBox.IsChecked = true;
        }

        private void ButtonUnselectAllparts_OnClick(object sender, RoutedEventArgs e)
        {
            foreach (var partBox in PartsPanel.Children.OfType<CheckBox>())
                partBox.IsChecked = false;
        }

        private void ButtonReload_OnClick(object sender, RoutedEventArgs e)
        {
            if (QueryReload != null)
                QueryReload(this, new EventArgs());
        }

        private void ButtonResetLight_Click(object sender, RoutedEventArgs e)
        {
            ResetLight();
        }

        private void ButtonResetGrid_Click(object sender, RoutedEventArgs e)
        {
            ResetGrid();
        }

        private void ButtonSnapNodes_OnClick(object sender, RoutedEventArgs e)
        {
            SnapAllNodes();
        }

        private void ButtonDuplicate_OnClick(object sender, RoutedEventArgs e)
        {
            DuplicateSelection(
                Math.Abs(SliderDupX.Value) > double.Epsilon,
                Math.Abs(SliderDupY.Value) > double.Epsilon,
                Math.Abs(SliderDupZ.Value) > double.Epsilon
            );
        }

        #endregion

        #region Colors & sizes

        private void ComboNodeColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Resources["NodeColor"] = (Color)(ComboNodeColor.SelectedItem as PropertyInfo).GetValue(null, null);
            Config.Map["3DNodeColor"] = (ComboNodeColor.SelectedItem as PropertyInfo).Name;
        }

        private void ComboBeamColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Resources["BeamColor"] = (Color)(ComboBeamColor.SelectedItem as PropertyInfo).GetValue(null, null);
            Config.Map["3DBeamColor"] = (ComboBeamColor.SelectedItem as PropertyInfo).Name;
        }

        private void ComboNameColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Resources["NodeNameBrush"] = (Brush)(ComboNameColor.SelectedItem as PropertyInfo).GetValue(null, null);
            Config.Map["3DNameColor"] = (ComboNameColor.SelectedItem as PropertyInfo).Name;
        }

        private void ComboTriangleColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var brush = (SolidColorBrush)((PropertyInfo)ComboTriangleColor.SelectedItem).GetValue(null, null);
            var color = Color.FromArgb(TriangleOpacity, brush.Color.R, brush.Color.G, brush.Color.B);
            Resources["TriangleBrush"] = new SolidColorBrush(color);
            Config.Map["3DTriangleColor"] = ((PropertyInfo)ComboTriangleColor.SelectedItem).Name;
        }

        private void ComboThrusterColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var brush = (SolidColorBrush)((PropertyInfo)ComboThrusterColor.SelectedItem).GetValue(null, null);
            Resources["ThrusterColor"] = brush;
            Config.Map["3DThrusterColor"] = ((PropertyInfo)ComboThrusterColor.SelectedItem).Name;
        }

        private void ComboCameraColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var brush = (SolidColorBrush)((PropertyInfo)ComboCameraColor.SelectedItem).GetValue(null, null);
            Resources["CameraBrush"] = brush;
            Config.Map["3DCameraColor"] = ((PropertyInfo)ComboCameraColor.SelectedItem).Name;
        }

        private void ComboGhostColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Resources["GhostColor"] = (Color)(ComboGhostColor.SelectedItem as PropertyInfo).GetValue(null, null);
            Config.Map["3DGhostColor"] = (ComboGhostColor.SelectedItem as PropertyInfo).Name;
        }

        private void ComboCursorColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Resources["CursorColor"] = (Color)(ComboCursorColor.SelectedItem as PropertyInfo).GetValue(null, null);
            Config.Map["3DCursorColor"] = (ComboCursorColor.SelectedItem as PropertyInfo).Name;
        }

        private void ComboSelectionColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Resources["CursorHitColor"] = (Color)(ComboSelectionColor.SelectedItem as PropertyInfo).GetValue(null, null);
            Config.Map["3DSelectionColor"] = (ComboSelectionColor.SelectedItem as PropertyInfo).Name;
        }

        private void ComboPartNodeColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Resources["PartNodeColor"] = (Color)(ComboPartNodeColor.SelectedItem as PropertyInfo).GetValue(null, null);
            Config.Map["3DPartNodeColor"] = (ComboPartNodeColor.SelectedItem as PropertyInfo).Name;
        }

        private void ComboPartBeamColor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Resources["PartBeamColor"] = (Color)((PropertyInfo)ComboPartBeamColor.SelectedItem).GetValue(null, null);
            Config.Map["3DPartBeamColor"] = ((PropertyInfo)ComboPartBeamColor.SelectedItem).Name;
        }

        private void SliderNodeSize_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            RefreshNodes();
            RefreshRef();
        }

        private void SliderBeamThickness_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            RefreshBeams();
            RefreshGhosts();
            RefreshHitBeam();
        }

        private void SliderPinLength_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            RefreshNames();
        }

        private void SliderNameSize_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            RefreshNames();
        }

        private void SliderTriangleOpacity_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (SliderTriangleOpacity == null) return;
            TriangleOpacity = (byte)Math.Round(SliderTriangleOpacity.Value);
        }

        #endregion

        #region Node & beam edition

        private void ViewTop_MouseDown(object sender, MouseButtonEventArgs e)
        {
            HandleMouseDown(new ViewInfo(
                TopViewport,
                TopGrid,
                RefTop,
                NodesTop,
                BeamsTop,
                TrianglesTop,
                ModelTop,
                GhostsTop,
                SnapValue
            ), e);
        }

        private void ViewFront_MouseDown(object sender, MouseButtonEventArgs e)
        {
            HandleMouseDown(new ViewInfo(
                FrontViewport,
                FrontGrid,
                RefFront,
                NodesFront,
                BeamsFront,
                TrianglesFront,
                ModelFront,
                GhostsFront,
                SnapValue
            ), e);
        }

        private void ViewRight_MouseDown(object sender, MouseButtonEventArgs e)
        {
            HandleMouseDown(new ViewInfo(
                RightViewport,
                RightGrid,
                RefRight,
                NodesRight,
                BeamsRight,
                TrianglesRight,
                ModelRight,
                GhostsRight,
                SnapValue
            ), e);
        }

        private void ViewPerspective_MouseDown(object sender, MouseButtonEventArgs e)
        {
            HandleMouseDown(new ViewInfo(
                PerspectiveViewport,
                PerspectiveGrid,
                RefPerspective,
                NodesPerspective,
                BeamsPerspective,
                TrianglesPerspective,
                ModelPerspective,
                GhostsPerspective,
                SnapValue
            ), e);
        }

        private void ViewTop_MouseMove(object sender, MouseEventArgs e)
        {
            HandleMouseMove(new ViewInfo(
                TopViewport,
                TopGrid,
                RefTop,
                NodesTop,
                BeamsTop,
                TrianglesTop,
                ModelTop,
                GhostsTop,
                SnapValue
            ), e);
        }

        private void ViewFront_MouseMove(object sender, MouseEventArgs e)
        {
            HandleMouseMove(new ViewInfo(
                FrontViewport,
                FrontGrid,
                RefFront,
                NodesFront,
                BeamsFront,
                TrianglesFront,
                ModelFront,
                GhostsFront,
                SnapValue
            ), e);
        }

        private void ViewRight_MouseMove(object sender, MouseEventArgs e)
        {
            HandleMouseMove(new ViewInfo(
                RightViewport,
                RightGrid,
                RefRight,
                NodesRight,
                BeamsRight,
                TrianglesRight,
                ModelRight,
                GhostsRight,
                SnapValue
            ), e);
        }

        private void ViewPerspective_MouseMove(object sender, MouseEventArgs e)
        {
            HandleMouseMove(new ViewInfo(
                PerspectiveViewport,
                PerspectiveGrid,
                RefPerspective,
                NodesPerspective,
                BeamsPerspective,
                TrianglesPerspective,
                ModelPerspective,
                GhostsPerspective,
                SnapValue
            ), e);
        }

        private void ViewTop_MouseUp(object sender, MouseButtonEventArgs e)
        {
            HandleMouseUp(new ViewInfo(
                TopViewport,
                TopGrid,
                RefTop,
                NodesTop,
                BeamsTop,
                TrianglesTop,
                ModelTop,
                GhostsTop,
                SnapValue
            ), e);
        }

        private void ViewFront_MouseUp(object sender, MouseButtonEventArgs e)
        {
            HandleMouseUp(new ViewInfo(
                FrontViewport,
                FrontGrid,
                RefFront,
                NodesFront,
                BeamsFront,
                TrianglesFront,
                ModelFront,
                GhostsFront,
                SnapValue
            ), e);
        }

        private void ViewRight_MouseUp(object sender, MouseButtonEventArgs e)
        {
            HandleMouseUp(new ViewInfo(
                RightViewport,
                RightGrid,
                RefRight,
                NodesRight,
                BeamsRight,
                TrianglesRight,
                ModelRight,
                GhostsRight,
                SnapValue
            ), e);
        }

        private void ViewPerspective_MouseUp(object sender, MouseButtonEventArgs e)
        {
            HandleMouseUp(new ViewInfo(
                PerspectiveViewport,
                PerspectiveGrid,
                RefPerspective,
                NodesPerspective,
                BeamsPerspective,
                TrianglesPerspective,
                ModelPerspective,
                GhostsPerspective,
                SnapValue
            ), e);
        }

        private void TopViewport_MouseEnter(object sender, MouseEventArgs e)
        {
            HandleMouseEnter(new ViewInfo(
                TopViewport,
                TopGrid,
                RefTop,
                NodesTop,
                BeamsTop,
                TrianglesTop,
                ModelTop,
                GhostsTop,
                SnapValue
            ), e);
        }

        private void RightViewport_MouseEnter(object sender, MouseEventArgs e)
        {
            HandleMouseEnter(new ViewInfo(
                RightViewport,
                RightGrid,
                RefRight,
                NodesRight,
                BeamsRight,
                TrianglesRight,
                ModelRight,
                GhostsRight,
                SnapValue
            ), e);
        }

        private void FrontViewport_MouseEnter(object sender, MouseEventArgs e)
        {
            HandleMouseEnter(new ViewInfo(
                FrontViewport,
                FrontGrid,
                RefFront,
                NodesFront,
                BeamsFront,
                TrianglesFront,
                ModelFront,
                GhostsFront,
                SnapValue
            ), e);
        }

        private void PerspectiveViewport_MouseEnter(object sender, MouseEventArgs e)
        {
            HandleMouseEnter(new ViewInfo(
                PerspectiveViewport,
                PerspectiveGrid,
                RefPerspective,
                NodesPerspective,
                BeamsPerspective,
                TrianglesPerspective,
                ModelPerspective,
                GhostsPerspective,
                SnapValue
            ), e);
        }

        private void SliderGridSize_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (PerspectiveGrid == null) return;
            PerspectiveGrid.Width = SliderGridSize.Value;
            PerspectiveGrid.Length = SliderGridSize.Value;
            TopGrid.Width = SliderGridSize.Value;
            TopGrid.Length = SliderGridSize.Value;
            FrontGrid.Width = SliderGridSize.Value;
            FrontGrid.Length = SliderGridSize.Value;
            RightGrid.Width = SliderGridSize.Value;
            RightGrid.Length = SliderGridSize.Value;
        }

        private void SliderGridSnap_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (PerspectiveGrid == null) return;
            var val = SliderGridSnap.Value;

            PerspectiveGrid.MinorDistance = val;
            PerspectiveGrid.MajorDistance = val * 10;
            TopGrid.MinorDistance = val;
            TopGrid.MajorDistance = val * 10;
            FrontGrid.MinorDistance = val;
            FrontGrid.MajorDistance = val * 10;
            RightGrid.MinorDistance = val;
            RightGrid.MajorDistance = val * 10;
        }

        private void SliderScale3D_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
        }

        private void SliderModelScale_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (QueryLoadModel != null)
                QueryLoadModel(this, new EventArgs());
        }

        private void SliderModelManipDiameter_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (ModelManipulator != null)
                ModelManipulator.Diameter = SliderModelManipDiameter.Value;
        }

        private void SliderLightIntensity_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (Light != null)
                Light.LinearAttenuation = e.NewValue;
        }

        #endregion

        #region Keyboard input & shortcuts

        private void Viewport_KeyDown(object sender, KeyEventArgs e)
        {
            /* Enable manipulator */
            if (e.Key == Key.O)
            {
                if (SelNodesPerspective.Points.Count == 0) return;
                Manipulator.Position = Avg(SelNodesPerspective.Points);
                Manipulator.Pivot = Manipulator.Position;
                Manipulator.Bind(SelNodesPerspective);
                IsSelectionActive = true;
            }

            /* Increase snapping */
            else if (e.Key == Key.C)
            {
                if (SliderGridSnap.Value < 0.1)
                    SliderGridSnap.Value += SliderGridSnap.SmallChange;
                else
                    SliderGridSnap.Value = SnapToValue(SliderGridSnap.Value + SliderGridSnap.SmallChange * 10, 0.1);
            }

            /* Decrease snapping */
            else if (e.Key == Key.X)
            {
                if (SliderGridSnap.Value <= 0.1)
                    SliderGridSnap.Value -= SliderGridSnap.SmallChange;
                else
                    SliderGridSnap.Value = SnapToValue(SliderGridSnap.Value - SliderGridSnap.SmallChange * 10, 0.1);
            }
        }

        private void Viewport_KeyUp(object sender, KeyEventArgs e)
        {
            /* Open radial menu */
            if (e.Key == Key.Space)
            {
                OpenRadialMenu(sender as HelixViewport3D);
            }

            /* Clear selection */
            else if (e.Key == Key.Enter)
            {
                ClearSelection();
            }

            /* Delete */
            else if (e.Key == Key.Delete)
            {
                /* Delete selection */
                if (IsSelectionActive)
                    DeleteSelection();

                /* Delete beam */
                else if (HitBeamPerspective.Points.Count() > 1)
                    RemoveHitBeam();

                /* Delete triangle */
                else if (HitTriangleGeometryPerspective.Positions.Any())
                    RemoveHitTriangle();
            }

            /* Switch cursor */
            else if (e.Key == Key.V)
            {
                if (Check3DCursor.IsChecked == true)
                    CheckGridCursor.IsChecked = true;
                else if (CheckGridCursor.IsChecked == true)
                    CheckNodeCursor.IsChecked = true;
                else
                    Check3DCursor.IsChecked = true;
            }

            /* Freeze / unfreeze 3D cursor */
            else if (e.Key == Key.E)
            {
                CheckShow3DCursor.IsChecked = !CheckShow3DCursor.IsChecked;
            }

            /* Toggle 'Add node' */
            else if (e.Key == Key.D1)
            {
                CheckAddNode.IsChecked = !CheckAddNode.IsChecked;
            }

            /* Toggle 'Add beam' */
            else if (e.Key == Key.D2)
            {
                CheckAddBeam.IsChecked = !CheckAddBeam.IsChecked;
            }

            /* Toggle 'Add triangle' */
            else if (e.Key == Key.D3)
            {
                CheckAddTriangle.IsChecked = !CheckAddTriangle.IsChecked;
            }

            /* Toggle 'Add thruster' */
            else if (e.Key == Key.D4)
            {
                CheckAddThruster.IsChecked = !CheckAddThruster.IsChecked;
            }

            /* Toggle 'Add camera' */
            else if (e.Key == Key.D5)
            {
                CheckAddCamera.IsChecked = !CheckAddCamera.IsChecked;
            }

            /* Grid */
            else if (e.Key == Key.G)
            {
                /* Show / hide grid */
                if (Keyboard.IsKeyDown(Key.LeftCtrl))
                    CheckShowGrid.IsChecked = !CheckShowGrid.IsChecked;

                /* Reset grid transform */
                else if (Keyboard.IsKeyDown(Key.LeftShift))
                    ResetGrid();

                /* Toggle move the grid */
                else
                    CheckMoveGrid.IsChecked = !CheckMoveGrid.IsChecked;
            }

            /* Toggle move the light */
            else if (e.Key == Key.L)
            {
                if (!Keyboard.IsKeyDown(Key.LeftShift))
                    CheckMoveLight.IsChecked = !CheckMoveLight.IsChecked;
                else
                    ResetLight();
            }

            /* Toggle triangles visibility */
            else if (e.Key == Key.T)
            {
                CheckShowTriangles.IsChecked = !CheckShowTriangles.IsChecked;
            }

            /* Toggle thrusters visibility */
            else if (e.Key == Key.Y)
            {
                CheckShowThrusters.IsChecked = !CheckShowThrusters.IsChecked;
            }

            /* Toggle cameras visibility */
            else if (e.Key == Key.U)
            {
                CheckShowCameras.IsChecked = !CheckShowCameras.IsChecked;
            }

            /* Toggle info visibility */
            else if (e.Key == Key.I)
            {
                CheckShowInfo.IsChecked = !CheckShowInfo.IsChecked;
            }

            /* Selection */
            else if (e.Key == Key.A)
            {
                /* Deselect all */
                if (Keyboard.IsKeyDown(Key.LeftCtrl) && Keyboard.IsKeyDown(Key.LeftShift))
                    ClearSelection();

                /* Select all */
                else if (Keyboard.IsKeyDown(Key.LeftCtrl))
                    SelectAll();
            }

            /* Model transform */
            else if (e.Key == Key.M)
            {
                /* Reset transform */
                if (Keyboard.IsKeyDown(Key.LeftShift))
                    ResetModelTransform();

                /* Toggle model visibility */
                else
                    CheckShowModel.IsChecked = !CheckShowModel.IsChecked;
            }
        }

        #endregion

        #region Radial Menu

        private void RadialMenu_CloseUp(object sender, EventArgs e)
        {
            RadialMenu.Visibility = System.Windows.Visibility.Hidden;
        }

        private void PmiToggleAddNode_Click(object sender, RoutedEventArgs e)
        {
            CheckAddNode.IsChecked = !CheckAddNode.IsChecked;
        }

        private void PmiToggleAddBeam_Click(object sender, RoutedEventArgs e)
        {
            CheckAddBeam.IsChecked = !CheckAddBeam.IsChecked;
        }

        private void PmiToggleAddTriangle_Click(object sender, RoutedEventArgs e)
        {
            CheckAddTriangle.IsChecked = !CheckAddTriangle.IsChecked;
        }

        private void PmiToggleAddThruster_Click(object sender, RoutedEventArgs e)
        {
            CheckAddThruster.IsChecked = !CheckAddThruster.IsChecked;
        }

        private void PmiToggleAddCamera_OnClick(object sender, RoutedEventArgs e)
        {
            CheckAddCamera.IsChecked = !CheckAddCamera.IsChecked;
        }

        private void PmiTogglePaintNodes_Click(object sender, RoutedEventArgs e)
        {
            CheckAutoNodes.IsChecked = !CheckAutoNodes.IsChecked;
        }

        private void PmiSnapOff_Click(object sender, RoutedEventArgs e)
        {
            SliderGridSnap.Value = 0;
        }

        private void PmiSnap05_Click(object sender, RoutedEventArgs e)
        {
            SliderGridSnap.Value = 0.5;
        }

        private void PmiSnap1_Click(object sender, RoutedEventArgs e)
        {
            SliderGridSnap.Value = 1;
        }

        private void PmiMoveGrid_Click(object sender, RoutedEventArgs e)
        {
            CheckMoveGrid.IsChecked = true;
        }

        private void PmiResetGrid_Click(object sender, RoutedEventArgs e)
        {
            ResetGrid();
        }

        private void PmiUseGridCursor_Click(object sender, RoutedEventArgs e)
        {
            CheckGridCursor.IsChecked = true;
        }

        private void PmiUse3DCursor_Click(object sender, RoutedEventArgs e)
        {
            Check3DCursor.IsChecked = true;
        }

        private void PmiUseNodeCursor_Click(object sender, RoutedEventArgs e)
        {
            CheckNodeCursor.IsChecked = true;
        }

        private void PmiToggleMouseCursor_Click(object sender, RoutedEventArgs e)
        {
            CheckShowMouseCursor.IsChecked = !CheckShowMouseCursor.IsChecked;
        }

        private void PmiToggle3DCursor_Click(object sender, RoutedEventArgs e)
        {
            CheckShow3DCursor.IsChecked = !CheckShow3DCursor.IsChecked;
        }

        private void PmiReloadAll_Click(object sender, RoutedEventArgs e)
        {
            ButtonReload_OnClick(ButtonReload, new RoutedEventArgs());
        }

        private void PmiMoveLight_Click(object sender, RoutedEventArgs e)
        {
            CheckMoveLight.IsChecked = true;
        }

        private void PmiResetLight_Click(object sender, RoutedEventArgs e)
        {
            ResetLight();
        }

        private void PmiClearSelection_Click(object sender, RoutedEventArgs e)
        {
            ClearSelection();
        }

        private void PmiDeleteSelection_Click(object sender, RoutedEventArgs e)
        {
            DeleteSelection();
        }

        private void PmiMirrorX_Click(object sender, RoutedEventArgs e)
        {
            MirrorSelection(true, false, false);
        }

        private void PmiMirrorY_Click(object sender, RoutedEventArgs e)
        {
            MirrorSelection(false, true, false);
        }

        private void PmiMirrorZ_Click(object sender, RoutedEventArgs e)
        {
            MirrorSelection(false, false, true);
        }

        private void PmiMirrorXY_Click(object sender, RoutedEventArgs e)
        {
            MirrorSelection(true, true, false);
        }

        private void PmiMirrorYZ_Click(object sender, RoutedEventArgs e)
        {
            MirrorSelection(false, true, true);
        }

        private void PmiMirrorXZ_Click(object sender, RoutedEventArgs e)
        {
            MirrorSelection(true, false, true);
        }

        private void PmiMirrorXYZ_Click(object sender, RoutedEventArgs e)
        {
            MirrorSelection(true, true, true);
        }

        private void PmiDupX_OnClick(object sender, RoutedEventArgs e)
        {
            DuplicateSelection(true, false, false);
        }

        private void PmiDupY_OnClick(object sender, RoutedEventArgs e)
        {
            DuplicateSelection(false, true, true);
        }

        private void PmiDupZ_OnClick(object sender, RoutedEventArgs e)
        {
            DuplicateSelection(false, false, true);
        }

        private void PmiDupXY_OnClick(object sender, RoutedEventArgs e)
        {
            DuplicateSelection(true, true, false);
        }

        private void PmiDupYZ_OnClick(object sender, RoutedEventArgs e)
        {
            DuplicateSelection(false, true, true);
        }

        private void PmiDupXZ_OnClick(object sender, RoutedEventArgs e)
        {
            DuplicateSelection(true, false, true);
        }

        private void PmiDupXYZ_OnClick(object sender, RoutedEventArgs e)
        {
            DuplicateSelection(true, true, true);
        }

        #endregion

        #endregion

        #region Methods

        #region Public methods

        #region Model

        public async void LoadModels(IEnumerable<string> paths)
        {
            //_models.Clear();
            ModelsPanel.Children.Clear();
            foreach (var path in paths)
            {
                /* Load the model */
                var model = await LoadModelAsync(path, false);
                model.Transform = new ScaleTransform3D(ModelScale, ModelScale, ModelScale);

                /* Store into list */
                var name = System.IO.Path.GetFileNameWithoutExtension(path);
                //_models.Add(name,  model);

                /* Add a checkbox */
                var checkBoxModel = new CheckBox()
                {
                    Name = string.Format("Check{0}", name),
                    Content = name,
                    Style = (Style)Resources["BarCheckStyle"],
                    MinHeight = 12,
                    Tag = model
                };
                ModelsPanel.Children.Add(checkBoxModel);

                /* Handle selection */
                checkBoxModel.Checked += (s, e) =>
                {
                    foreach (var box in ModelsPanel.Children.OfType<CheckBox>())
                    {
                        if (box != s) box.IsChecked = false;
                    }

                    CurrentModel = ((CheckBox)s).Tag as Model3DGroup;
                };
                checkBoxModel.Unchecked += (s, e) =>
                {
                    HideModel();
                };
            }
        }

        public async void LoadModel(string path)
        {
            CurrentModel = await LoadModelAsync(path, false);
            CurrentModel.Transform = new ScaleTransform3D(ModelScale, ModelScale, ModelScale);

            Bind3DModel();
        }

        #endregion

        #region Add

        public void AddNode(Point3D node, bool ignoreEvents = false)
        {
            /* Snap to grid */
            node = SnapPoint(node, 0, PerspectiveGrid.Center);

            /* Node already exists */
            if (NodesPerspective.Points.Contains(node))
            {
                return;
            }

            /* Create a new node collection and add the new one */
            var points = new Point3DCollection(NodesPerspective.Points) { node };

            /* Reset nodes */
            NodesPerspective.Points = points;
            NodesTop.Points = points;
            NodesFront.Points = points;
            NodesRight.Points = points;

            ///* Update node names */
            //var name = new BillboardTextItem { Text = "node", Position = node };
            //var names = new List<BillboardTextItem>(NodeNamesPerspective.Items ?? new List<BillboardTextItem>()) { name };
            //NodeNamesPerspective.Items = names;
            //NodeNamesTop.Items = names;
            //NodeNamesFront.Items = names;
            //NodeNamesRight.Items = names;

            /* Ignore events */
            if (ignoreEvents) return;

            /* Notify */
            if (_movingNode.HasValue && NodeMoved != null)
                NodeMoved(this, new Tuple<Point3D, Point3D>(_movingNode.Value, node));
            else if (!_movingNode.HasValue && NodeAdded != null)
                NodeAdded(this, node);
        }

        public void AddNodes(IEnumerable<Point3D> nodes, bool ignoreEvents = false)
        {
            foreach (var node in nodes)
                AddNode(node, ignoreEvents);
        }

        public void AddBeam(Point3D start, Point3D end, bool ignoreEvents = false)
        {
            /* Snap to grid */
            start = SnapPoint(start, 0, PerspectiveGrid.Center);
            end = SnapPoint(end, 0, PerspectiveGrid.Center);

            /* Create a new beam collection and add the new points */
            var points = new Point3DCollection(BeamsPerspective.Points);
            points.Add(start);
            points.Add(end);

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;

            /* Ignore events */
            if (ignoreEvents) return;

            /* Notify */
            if (BeamAdded != null)
                BeamAdded(this, new Tuple<Point3D, Point3D>(start, end));
        }

        public void AddBeams(IEnumerable<Point3D> nodes, bool ignoreEvents = false)
        {
            /* Create a new beam collection and add the new points */
            var points = new Point3DCollection(BeamsPerspective.Points);
            foreach (var node in nodes)
                points.Add(SnapPoint(node, 0, PerspectiveGrid.Center));

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;

            /* Ignore events */
            if (ignoreEvents) return;

            /* Notify */
            if (BeamsAdded != null)
                BeamsAdded(this, new EventArgs());
        }

        public void AddPart(string partName, IEnumerable<Point3D> nodes, IEnumerable<Point3D> beams)
        {
            var nodePoints = new Point3DCollection();
            var beamPoints = new Point3DCollection();

            foreach (var node in nodes)
                nodePoints.Add(SnapPoint(node, 0, PerspectiveGrid.Center));
            foreach (var beam in beams)
                beamPoints.Add(SnapPoint(beam, 0, PerspectiveGrid.Center));

            var nodeColor = (Color)Resources["PartNodeColor"];
            var beamColor = (Color)Resources["PartBeamColor"];

            var nodesPerspective = new PointsVisual3D() { Points = nodePoints, Color = nodeColor, Size = 6 };
            var nodesTop = new PointsVisual3D() { Points = nodePoints, Color = nodeColor, Size = 6 };
            var nodesFront = new PointsVisual3D() { Points = nodePoints, Color = nodeColor, Size = 6 };
            var nodesRight = new PointsVisual3D() { Points = nodePoints, Color = nodeColor, Size = 6 };

            var beamsPerspective = new LinesVisual3D() { Points = beamPoints, Color = beamColor, Thickness = 2 };
            var beamsTop = new LinesVisual3D() { Points = beamPoints, Color = beamColor, Thickness = 2 };
            var beamsFront = new LinesVisual3D() { Points = beamPoints, Color = beamColor, Thickness = 2 };
            var beamsRight = new LinesVisual3D() { Points = beamPoints, Color = beamColor, Thickness = 2 };

            nodesPerspective.SetName(string.Format("{0}NodesPerspective", partName));
            nodesTop.SetName(string.Format("{0}NodesTop", partName));
            nodesFront.SetName(string.Format("{0}NodesFront", partName));
            nodesRight.SetName(string.Format("{0}NodesRight", partName));

            beamsPerspective.SetName(string.Format("{0}BeamsPerspective", partName));
            beamsTop.SetName(string.Format("{0}BeamsTop", partName));
            beamsFront.SetName(string.Format("{0}BeamsFront", partName));
            beamsRight.SetName(string.Format("{0}BeamsRight", partName));

            PerspectiveViewport.Children.Insert(4, nodesPerspective);
            TopViewport.Children.Insert(4, nodesTop);
            FrontViewport.Children.Insert(4, nodesFront);
            RightViewport.Children.Insert(4, nodesRight);

            PerspectiveViewport.Children.Insert(4, beamsPerspective);
            TopViewport.Children.Insert(4, beamsTop);
            FrontViewport.Children.Insert(4, beamsFront);
            RightViewport.Children.Insert(4, beamsRight);
        }

        public void AddTriangle(Point3D node1, Point3D node2, Point3D node3, bool ignoreEvents = false)
        {
            /* Snap to grid */
            node1 = SnapPoint(node1, 0, PerspectiveGrid.Center);
            node2 = SnapPoint(node2, 0, PerspectiveGrid.Center);
            node3 = SnapPoint(node3, 0, PerspectiveGrid.Center);

            /* Create new triangle positions */
            var positions = new Point3DCollection(TriangleGeometryPerspective.Positions) { node1, node2, node3 };

            /* Create new index list */
            var indices = new Int32Collection(TriangleGeometryPerspective.TriangleIndices)
            {
                positions.Count - 3,
                positions.Count - 2,
                positions.Count - 1
            };

            /* Create new normal list */
            var n = CalculateTriangleNormal(node1, node2, node3);
            var normals = new Vector3DCollection(TriangleGeometryPerspective.Normals) { n, n, n };

            /* Update geometry */
            TriangleGeometryPerspective.Positions = positions;
            TriangleGeometryTop.Positions = positions;
            TriangleGeometryFront.Positions = positions;
            TriangleGeometryRight.Positions = positions;
            TriangleGeometryPerspective.TriangleIndices = indices;
            TriangleGeometryTop.TriangleIndices = indices;
            TriangleGeometryFront.TriangleIndices = indices;
            TriangleGeometryRight.TriangleIndices = indices;
            TriangleGeometryPerspective.Normals = normals;
            TriangleGeometryTop.Normals = normals;
            TriangleGeometryFront.Normals = normals;
            TriangleGeometryRight.Normals = normals;

            /* Update visual normals */
            if (CheckShowNormals.IsChecked == true)
                UpdateNormals();

            /* Ignore events */
            if (ignoreEvents) return;

            /* Notify */
            if (TriangleAdded != null)
                TriangleAdded(this, new Tuple<Point3D, Point3D, Point3D>(node1, node2, node3));
        }

        public void AddTriangles(IEnumerable<Tuple<Point3D, Point3D, Point3D>> triangles, bool ignoreEvents = false)
        {
            foreach (var triangle in triangles)
                AddTriangle(triangle.Item1, triangle.Item2, triangle.Item3, ignoreEvents);
        }

        public void AddThruster(Point3D start, Point3D end, bool ignoreEvents = false)
        {
            /* Snap to grid */
            start = SnapPoint(start, 0, PerspectiveGrid.Center);
            end = SnapPoint(end, 0, PerspectiveGrid.Center);

            /* Create a new pipe visual based on the template */
            var thrusterPerspective = ClonePipe(ThrusterTemplatePerspective, start, end);
            var thrusterTop = ClonePipe(ThrusterTemplatePerspective, start, end);
            var thrusterFront = ClonePipe(ThrusterTemplatePerspective, start, end);
            var thrusterRight = ClonePipe(ThrusterTemplatePerspective, start, end);

            /* Add it to the viewports */
            PerspectiveViewport.Children.Insert(
                PerspectiveViewport.Children.IndexOf(BeamsPerspective) + 1,
                thrusterPerspective
            );
            TopViewport.Children.Insert(
                TopViewport.Children.IndexOf(BeamsTop) + 1,
                thrusterTop
            );
            FrontViewport.Children.Insert(
                FrontViewport.Children.IndexOf(BeamsFront) + 1,
                thrusterFront
            );
            RightViewport.Children.Insert(
                RightViewport.Children.IndexOf(BeamsRight) + 1,
                thrusterRight
            );

            /* Ignore events */
            if (ignoreEvents) return;

            /* Notify */
            if (ThrusterAdded != null)
                ThrusterAdded(this, new Tuple<Point3D, Point3D>(start, end));
        }

        /// <summary>Add multiple thrusters</summary>
        /// <param name="nodes"></param>
        /// <param name="ignoreEvents"></param>
        public void AddThrusters(IEnumerable<Point3D> nodes, bool ignoreEvents = false)
        {
            for (var i = 0; i < nodes.Count(); i += 2)
                AddThruster(nodes.ElementAt(i), nodes.ElementAt(i + 1), true);
        }

        /// <summary>Adds a camera</summary>
        /// <param name="origin"></param>
        /// <param name="fov"></param>
        /// <param name="ignoreEvents"></param>
        public void AddCamera(Point3D origin, float fov, bool ignoreEvents = false)
        {
            AddCamera(origin, fov, null, ignoreEvents);
        }

        /// <summary>Adds a camera</summary>
        /// <param name="origin"></param>
        /// <param name="fov"></param>
        /// <param name="ignoreEvents"></param>
        public void AddCamera(Point3D origin, float fov, Tuple<Point3D, Point3D, Point3D, Point3D, Point3D, Point3D> links, bool ignoreEvents = false)
        {
            /* Snap to grid */
            origin = SnapPoint(origin, 0, PerspectiveGrid.Center);

            /* Create a new visual based on the template */
            var cameraPerspective = CloneCone(CameraTemplatePerspective, origin, fov);
            var cameraTop = CloneCone(CameraTemplatePerspective, origin, fov);
            var cameraFront = CloneCone(CameraTemplatePerspective, origin, fov);
            var cameraRight = CloneCone(CameraTemplatePerspective, origin, fov);

            /* Add it to the viewports */
            PerspectiveViewport.Children.Insert(
                PerspectiveViewport.Children.IndexOf(NodesPerspective) + 1,
                cameraPerspective
            );
            TopViewport.Children.Insert(
                TopViewport.Children.IndexOf(NodesTop) + 1,
                cameraTop
            );
            FrontViewport.Children.Insert(
                FrontViewport.Children.IndexOf(NodesFront) + 1,
                cameraFront
            );
            RightViewport.Children.Insert(
                RightViewport.Children.IndexOf(NodesRight) + 1,
                cameraRight
            );

            /* Update links */
            if (links != null)
                UpdateCameraLinks(origin, links.Item1, links.Item2, links.Item3, links.Item4, links.Item5, links.Item6);

            /* Ignore events */
            if (ignoreEvents) return;

            /* Notify */
            if (ExternalCameraChanged != null && links == null)
                ExternalCameraChanged(this, new Tuple<Point3D, float>(origin, fov));
            else if (InternalCameraAdded != null && links != null)
                InternalCameraAdded(this, new Tuple<Point3D, float, Tuple<Point3D, Point3D, Point3D, Point3D, Point3D, Point3D>>(
                    origin, fov, new Tuple<Point3D, Point3D, Point3D, Point3D, Point3D, Point3D>(
                        links.Item1, links.Item2, links.Item3, links.Item4, links.Item5, links.Item6)));
        }

        #endregion

        #region Remove

        public void RemoveNode(Point3D node, bool ignoreEvents = false)
        {
            /* Node doesn't exist */
            if (!NodesPerspective.Points.Contains(node)) return;

            /* Create a new node collection and remove the given one */
            var points = new Point3DCollection(NodesPerspective.Points);
            points.Remove(node);

            /* Reset nodes */
            NodesPerspective.Points = points;
            NodesTop.Points = points;
            NodesFront.Points = points;
            NodesRight.Points = points;

            ///* Update node names */
            //var names = new List<BillboardTextItem>(NodeNamesPerspective.Items ?? new List<BillboardTextItem>());
            //names.Remove(names.FirstOrDefault(name => name.Position == node));
            //NodeNamesPerspective.Items = names;
            //NodeNamesTop.Items = names;
            //NodeNamesFront.Items = names;
            //NodeNamesRight.Items = names;

            /* Delete linked beams */
            RemoveLinkedBeams(node);

            /* Delete linked triangles */
            RemoveLinkedTriangles(node);

            /* Notify */
            if (!_movingNode.HasValue && NodeRemoved != null && !ignoreEvents)
                NodeRemoved(this, node);
        }

        public void RemoveNodes(IEnumerable<Point3D> nodes, bool ignoreEvents = false)
        {
            foreach (var node in nodes)
                RemoveNode(node, ignoreEvents);
        }

        public void RemoveBeam(Point3D start, Point3D end, bool ignoreEvents = false)
        {
            /* Snap to grid */
            start = SnapPoint(start, 0);
            end = SnapPoint(end, 0);

            /* Create a new beam collection and add the new points */
            var points = new Point3DCollection(BeamsPerspective.Points);

            /* Remove all pairs containing the given nodes */
            for (var bid = 0; bid < points.Count; bid += 2)
            {
                var pt1 = points[bid];
                var pt2 = points[bid + 1];

                if ((!pt1.Equals(start) || !pt2.Equals(end)) && (!pt2.Equals(start) || !pt2.Equals(end))) continue;

                points.RemoveAt(bid);
                points.RemoveAt(bid);

                /* Notify */
                if (BeamRemoved != null && !ignoreEvents)
                    BeamRemoved(this, new Tuple<Point3D, Point3D>(pt1, pt2));

                bid -= 2;
            }

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;
        }

        public void RemoveBeams(Point3D start, Point3D end, bool ignoreEvents = false)
        {
            /* Snap to grid */
            start = SnapPoint(start, 0);
            end = SnapPoint(end, 0);

            /* Create a new beam collection and add the new points */
            var points = new Point3DCollection(BeamsPerspective.Points);
            points.Remove(start);
            points.Remove(end);

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;

            /* Ignore events */
            if (ignoreEvents) return;

            /* Notify */
            if (BeamRemoved != null)
                BeamRemoved(this, new Tuple<Point3D, Point3D>(start, end));
        }

        public void RemovePart(string partName)
        {
            var nodesPerspective = PerspectiveViewport.Children.FirstOrDefault(v => v.GetName() != null && v.GetName().Equals(string.Format("{0}NodesPerspective", partName)));
            var nodesTop = TopViewport.Children.FirstOrDefault(v => v.GetName() != null && v.GetName().Equals(string.Format("{0}NodesTop", partName)));
            var nodesFront = FrontViewport.Children.FirstOrDefault(v => v.GetName() != null && v.GetName().Equals(string.Format("{0}NodesFront", partName)));
            var nodesRight = RightViewport.Children.FirstOrDefault(v => v.GetName() != null && v.GetName().Equals(string.Format("{0}NodesRight", partName)));
            var beamsPerspective = PerspectiveViewport.Children.FirstOrDefault(v => v.GetName() != null && v.GetName().Equals(string.Format("{0}BeamsPerspective", partName)));
            var beamsTop = TopViewport.Children.FirstOrDefault(v => v.GetName() != null && v.GetName().Equals(string.Format("{0}BeamsTop", partName)));
            var beamsFront = FrontViewport.Children.FirstOrDefault(v => v.GetName() != null && v.GetName().Equals(string.Format("{0}BeamsFront", partName)));
            var beamsRight = RightViewport.Children.FirstOrDefault(v => v.GetName() != null && v.GetName().Equals(string.Format("{0}BeamsRight", partName)));

            if (nodesPerspective == null) return;

            PerspectiveViewport.Children.Remove(nodesPerspective);
            TopViewport.Children.Remove(nodesTop);
            FrontViewport.Children.Remove(nodesFront);
            RightViewport.Children.Remove(nodesRight);
            PerspectiveViewport.Children.Remove(beamsPerspective);
            TopViewport.Children.Remove(beamsTop);
            FrontViewport.Children.Remove(beamsFront);
            RightViewport.Children.Remove(beamsRight);
        }

        public void RemoveTriangle(int index1, int index2, int index3, bool ignoreEvents = false)
        {
            var curPositions = TriangleGeometryPerspective.Positions;
            var curNormals = TriangleGeometryPerspective.Normals;

            var positions = new Point3DCollection();
            var normals = new Vector3DCollection();
            var indices = new Int32Collection();

            /* Extract positions */
            var node1 = curPositions[index1];
            var node2 = curPositions[index2];
            var node3 = curPositions[index3];

            for (var i = 0; i < curPositions.Count; i++)
            {
                if (i != index1 && i != index2 && i != index3)
                {
                    positions.Add(curPositions[i]);
                    normals.Add(curNormals[i]);
                    indices.Add(indices.Count);
                }
            }

            TriangleGeometryPerspective.Positions = positions;
            TriangleGeometryPerspective.Normals = normals;
            TriangleGeometryPerspective.TriangleIndices = indices;

            TriangleGeometryTop.Positions = positions;
            TriangleGeometryTop.Normals = normals;
            TriangleGeometryTop.TriangleIndices = indices;

            TriangleGeometryFront.Positions = positions;
            TriangleGeometryFront.Normals = normals;
            TriangleGeometryFront.TriangleIndices = indices;

            TriangleGeometryRight.Positions = positions;
            TriangleGeometryRight.Normals = normals;
            TriangleGeometryRight.TriangleIndices = indices;

            /* Update visual normals */
            if (CheckShowNormals.IsChecked == true)
                UpdateNormals();

            /* Ignore events */
            if (ignoreEvents) return;

            /* Notify */
            if (TriangleRemoved != null)
                TriangleRemoved(this, new Tuple<Point3D, Point3D, Point3D>(node1, node2, node3));
        }

        public void RemoveTriangle(Point3D node1, Point3D node2, Point3D node3, bool ignoreEvents = false)
        {
            /* Snap to grid */
            node1 = SnapPoint(node1, 0, PerspectiveGrid.Center);
            node2 = SnapPoint(node2, 0, PerspectiveGrid.Center);
            node3 = SnapPoint(node3, 0, PerspectiveGrid.Center);

            /* Create new triangle positions */
            var positions = new Point3DCollection(TriangleGeometryPerspective.Positions);

            /* Create new index list */
            var indices = new Int32Collection(TriangleGeometryPerspective.TriangleIndices);
            var triangleIndices = new Int32Collection();
            for (var i = 0; i < indices.Count; i += 3)
            {
                var ok1 =
                    positions[indices[i + 0]].Equals(node1) ||
                    positions[indices[i + 0]].Equals(node2) ||
                    positions[indices[i + 0]].Equals(node3);
                var ok2 =
                    positions[indices[i + 1]].Equals(node1) ||
                    positions[indices[i + 1]].Equals(node2) ||
                    positions[indices[i + 1]].Equals(node3);
                var ok3 =
                    positions[indices[i + 2]].Equals(node1) ||
                    positions[indices[i + 2]].Equals(node2) ||
                    positions[indices[i + 2]].Equals(node3);

                if (ok1 && ok2 && ok3)
                {
                    triangleIndices.Add(indices[i + 0]);
                    triangleIndices.Add(indices[i + 1]);
                    triangleIndices.Add(indices[i + 2]);
                    break;
                }
            }

            /* Not found */
            if (triangleIndices.Count < 3) return;

            /* Delete by indices */
            RemoveTriangle(triangleIndices[0], triangleIndices[1], triangleIndices[2]);
        }

        public void RemoveCamera(Point3D origin, bool ignoreEvents = false)
        {
            var toDelete = PerspectiveViewport.Children.Where(child => child is TruncatedConeVisual3D && child != CameraTemplatePerspective && ((TruncatedConeVisual3D)child).Origin.Equals(origin)).ToList();
            while (toDelete.Count > 0)
            {
                PerspectiveViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = TopViewport.Children.Where(child => child is TruncatedConeVisual3D && ((TruncatedConeVisual3D)child).Origin.Equals(origin)).ToList();
            while (toDelete.Count > 0)
            {
                TopViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = FrontViewport.Children.Where(child => child is TruncatedConeVisual3D && ((TruncatedConeVisual3D)child).Origin.Equals(origin)).ToList();
            while (toDelete.Count > 0)
            {
                FrontViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = RightViewport.Children.Where(child => child is TruncatedConeVisual3D && ((TruncatedConeVisual3D)child).Origin.Equals(origin)).ToList();
            while (toDelete.Count > 0)
            {
                RightViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }
        }

        #endregion

        #region Clear

        public void ClearNodes(bool ignoreEvents = false)
        {
            /* Create a new node collection and remove the given one */
            var points = new Point3DCollection();

            /* Reset nodes */
            NodesPerspective.Points = points;
            NodesTop.Points = points;
            NodesFront.Points = points;
            NodesRight.Points = points;

            /* Clear names */
            ClearNames();

            /* Clear beams */
            ClearBeams();
        }

        public void ClearBeams()
        {
            /* Create a new beams collection */
            var points = new Point3DCollection();

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;
        }

        public void ClearNames()
        {
            var names = new List<BillboardTextItem>(new List<BillboardTextItem>());

            NodeNamesPerspective.Items = names;
            NodeNamesTop.Items = names;
            NodeNamesFront.Items = names;
            NodeNamesRight.Items = names;
        }

        public void ClearNormals()
        {
            NormalsPerspective.Points = new Point3DCollection();
        }

        public void ClearTriangles()
        {
            /* Create empty collections */
            var positions = new Point3DCollection();
            var indices = new Int32Collection();
            var normals = new Vector3DCollection();

            /* Reset triangles */
            TriangleGeometryPerspective.Positions = positions;
            TriangleGeometryTop.Positions = positions;
            TriangleGeometryFront.Positions = positions;
            TriangleGeometryRight.Positions = positions;
            TriangleGeometryPerspective.TriangleIndices = indices;
            TriangleGeometryTop.TriangleIndices = indices;
            TriangleGeometryFront.TriangleIndices = indices;
            TriangleGeometryRight.TriangleIndices = indices;
            TriangleGeometryPerspective.Normals = normals;
            TriangleGeometryTop.Normals = normals;
            TriangleGeometryFront.Normals = normals;
            TriangleGeometryRight.Normals = normals;
        }

        public void ClearThrusters()
        {
            var toDelete = PerspectiveViewport.Children.Where(child => child is PipeVisual3D && child != ThrusterTemplatePerspective).ToList();
            while (toDelete.Count > 0)
            {
                PerspectiveViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = TopViewport.Children.Where(child => child is PipeVisual3D).ToList();
            while (toDelete.Count > 0)
            {
                TopViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = FrontViewport.Children.Where(child => child is PipeVisual3D).ToList();
            while (toDelete.Count > 0)
            {
                FrontViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = RightViewport.Children.Where(child => child is PipeVisual3D).ToList();
            while (toDelete.Count > 0)
            {
                RightViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }
        }

        public void ClearCameras()
        {
            var toDelete = PerspectiveViewport.Children.Where(child => child is TruncatedConeVisual3D && child != CameraTemplatePerspective).ToList();
            while (toDelete.Count > 0)
            {
                PerspectiveViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = TopViewport.Children.Where(child => child is TruncatedConeVisual3D).ToList();
            while (toDelete.Count > 0)
            {
                TopViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = FrontViewport.Children.Where(child => child is TruncatedConeVisual3D).ToList();
            while (toDelete.Count > 0)
            {
                FrontViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            toDelete = RightViewport.Children.Where(child => child is TruncatedConeVisual3D).ToList();
            while (toDelete.Count > 0)
            {
                RightViewport.Children.Remove(toDelete[0]);
                toDelete.RemoveAt(0);
            }

            /* Clear links */
            ClearCameraLinks();
        }

        #endregion

        #region Update

        public void SetNames(Point3D node, IEnumerable<string> nodeNames)
        {
            /* Update node names */
            var names = new List<BillboardTextItem>(NodeNamesPerspective.Items ?? new List<BillboardTextItem>());
            names.RemoveAll(name => name.Position == node);
            if (nodeNames != null && nodeNames.Any())
                names.AddRange(nodeNames.Select(name => new BillboardTextItem { Text = name, Position = node }));

            NodeNamesPerspective.Items = names;
            NodeNamesTop.Items = names;
            NodeNamesFront.Items = names;
            NodeNamesRight.Items = names;
        }

        public void UpdateNames(IEnumerable<Tuple<Point3D, string>> namedNodes)
        {
            var names = new List<BillboardTextItem>(NodeNamesPerspective.Items ?? new List<BillboardTextItem>());
            names.AddRange(namedNodes.Select(tuple => new BillboardTextItem() { Text = tuple.Item2, Position = SnapPoint(tuple.Item1, 0, PerspectiveGrid.Center) }));

            NodeNamesPerspective.Items = names;
            NodeNamesTop.Items = names;
            NodeNamesFront.Items = names;
            NodeNamesRight.Items = names;
        }

        public void SetPartsList(IEnumerable<Tuple<string, string>> parts)
        {
            PartsPanel.Children.Clear();
            foreach (var part in parts)
            {
                var checkBoxPart = new CheckBox()
                {
                    Name = string.Format("Check{0}", part.Item1),
                    Content = part.Item2,
                    Style = (Style)Resources["BarCheckStyle"],
                    MinHeight = 12,
                    Tag = part.Item1
                };
                PartsPanel.Children.Add(checkBoxPart);

                checkBoxPart.Checked += CheckBoxPart_Checked;
                checkBoxPart.Unchecked += CheckBoxPart_Unchecked;
            }
        }

        public void UpdateNormals()
        {
            if (TriangleGeometryPerspective == null) return;

            var visualNormals = new Point3DCollection();
            var normals = TriangleGeometryPerspective.Normals;
            var positions = TriangleGeometryPerspective.Positions;
            for (var i = 0; i < positions.Count; i++)
            {
                visualNormals.Add(positions[i]);
                visualNormals.Add(new Point3D(positions[i].X + normals[i].X, positions[i].Y + normals[i].Y, positions[i].Z + normals[i].Z));
            }

            NormalsPerspective.Points = visualNormals;
        }

        public void UpdateCameraLinks(Point3D origin, params Point3D[] links)
        {
            /* Remove current links */
            RemoveCameraLinks(origin);

            if (links == null || links.Length < 6) return;

            /* Create a new camera links collection */
            var points = new Point3DCollection(CameraLinksPerspective.Points);

            /* Add links */
            for (var i = 0; i < 6; i++)
            {
                points.Add(origin);
                points.Add(links[i]);
            }

            /* Update visuals */
            CameraLinksPerspective.Points = points;
            CameraLinksTop.Points = points;
            CameraLinksFront.Points = points;
            CameraLinksRight.Points = points;
        }

        public void UpdateVisibility()
        {
            SetGridVisible(CheckShowGrid.IsChecked == true);
            SetThrustersVisible(CheckShowThrusters.IsChecked == true);
            SetCamerasVisible(CheckShowCameras.IsChecked == true);

            if (CheckShowTriangles.IsChecked == true)
            {
                if (QueryTriangles != null)
                    QueryTriangles(this, new EventArgs());
            }
            else ClearTriangles();

            if (CheckShowNormals.IsChecked == true)
                UpdateNormals();
            else
                ClearNormals();
        }

        #endregion

        #region ToolTip

        public void SetNodeInfo(Point3D point, string text)
        {
            //ShowToolTip(point, text);
            ShowOnHUD(text);
        }

        public void SetBeamInfo(Point3D node1, Point3D node2, string text)
        {
            //ShowToolTip(CursorPosition, text);
            ShowOnHUD(text);
        }

        public void SetTriangleInfo(Point3D node1, Point3D node2, Point3D node3, string text)
        {
            //ShowToolTip(CursorPosition, text);
            ShowOnHUD(text);
        }

        public void ClearToolTip()
        {
            HideToolTip();
            ShowOnHUD(null);
        }

        #endregion

        #endregion

        #region Private methods

        #region Model related

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }

        private async Task<Model3DGroup> LoadModelAsync(string path, bool freeze)
        {
            return await Task.Factory.StartNew(() =>
            {
                var mi = new ModelImporter();

                return freeze
                    ? mi.Load(path, null, true)
                    : mi.Load(path, _dispatcher);
            });
        }

        private void Bind3DModel()
        {
            /* Bind manipulator */
            if (CurrentModel != null)
            {
                ModelManipulator.Position = new Point3D(0, 0, 0);
                ModelManipulator.Pivot = new Point3D(0, 0, 0);
                ModelManipulator.Bind(ModelPerspective);
            }

            else
            {
                ModelManipulator.UnBind();
                ModelManipulator.Position = new Point3D(double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity);
            }
        }

        private void ReloadModel()
        {
            if (QueryLoadModel != null)
                QueryLoadModel(this, new EventArgs());
        }

        private void HideModel()
        {
            CurrentModel = null;
        }

        private void ResetModelTransform()
        {
            ModelManipulator.TargetTransform = Transform3D.Identity;
        }

        #endregion

        #region Global mouse events

        private void HandleMouseDown(ViewInfo info, MouseButtonEventArgs e)
        {
            /* Right button is used to pan view */
            if (e.RightButton == MouseButtonState.Pressed) return;

            /* Do nothing if we're manipulating the nodes */
            if (Keyboard.IsKeyDown(Key.Space)) return;

            /* Moving the grid */
            if (CheckMoveGrid.IsChecked == true) return;

            /* Get cursor position */
            var curPos = GetCurPos(info, e);

            /* Get target pos */
            var pos = GetAimPoint(info, curPos);

            /* Select a node */
            if (pos.HasValue && IsPositionOnNode(info, pos.Value) && Keyboard.IsKeyDown(Key.LeftCtrl))
            {
                SelectNode(pos.Value);
                return;
            }

            /* Place a new internal camera if enabled */
            if (CheckAddCamera.IsChecked == true && _curCameraPerspective == null && SelNodesPerspective.Points.Count == 6)
                PlaceCamera(info, curPos);

            /* Do nothing else if we're manipulating nodes */
            if (SelNodesPerspective.Points.Count > 0) return;

            /* Place a node if adding nodes is enabled */
            if (CheckAddNode.IsChecked == true)
                PlaceNode(info, curPos);

            /* Store beam start */
            if (CheckAddBeam.IsChecked == true && !_lineStart.HasValue)
                _lineStart = CheckNodeCursor.IsChecked == true || Check3DCursor.IsChecked == true ? _cursorPoint : GetAimPoint(info, curPos);

            /* Place a new thruster if enabled */
            if (CheckAddThruster.IsChecked == true && _curThrusterPerspective == null)
                PlaceThruster(info, curPos);

            /* Place a new camera if enabled */
            if (CheckAddCamera.IsChecked == true && _curCameraPerspective == null)
                PlaceCamera(info, curPos);
        }

        private void HandleMouseEnter(ViewInfo info, MouseEventArgs e)
        {
            /* Focus the viewport */
            info.View.Focus();
        }

        private void HandleMouseMove(ViewInfo info, MouseEventArgs e)
        {
            /* Do nothing if we're using the radial menu */
            if (Keyboard.IsKeyDown(Key.Space)) return;

            /* Get cursor position */
            var curPos = GetCurPos(info, e);

            /* Hide tooltip before it is shown by something else */
            ClearToolTip();

            /* Update camera currently being added */
            if (_curCameraPerspective != null)
                UpdateNewCamera(info, curPos);

            /* Apply transforms to selection */
            if (!Keyboard.IsKeyDown(Key.LeftCtrl) && SelNodesPerspective.Points.Count > 0 && e.LeftButton == MouseButtonState.Pressed)
            {
                TransformNodes(Manipulator.Transform);
                TransformNames();
                return;
            }

            /* Set cursor point */
            var cursorPos = SetCurPoint(info, curPos);

            /* Move the grid */
            if (CheckMoveGrid.IsChecked == true)
            {
                MoveGrid(info, curPos);
                return;
            }

            /* Move the light */
            if (CheckMoveLight.IsChecked == true)
            {
                MoveLight(info, curPos);
                RefPerspective.Points.Clear();
            }

            /* Don't do anything if something is selected */
            if (SelNodesPerspective.Points.Count > 0)
                return;

            /* Show hit triangle */
            UpdateHitTriangle(info, curPos);

            /* Show hit beam */
            UpdateHitBeam(info, curPos);

            /* Place a node if adding nodes is enabled & mouse is down & continuous mode is on */
            if (e.LeftButton == MouseButtonState.Pressed &&
                CheckAddNode.IsChecked == true &&
                CheckAutoNodes.IsChecked == true)
            {
                PlaceNode(info, curPos);
                return;
            }

            /* Show ghost nodes */
            if (e.LeftButton == MouseButtonState.Pressed)
                ShowGhosts(info, curPos);

            /* Update thruster currently being added */
            if (_curThrusterPerspective != null)
                UpdateNewThruster(info, curPos);
        }

        private void HandleMouseUp(ViewInfo info, MouseButtonEventArgs e)
        {
            /* Right button is used to pan view */
            if (e.ChangedButton != MouseButton.Left) return;

            /* In any case remove ghosts */
            ClearGhosts();

            /* Moving grid */
            if (CheckMoveGrid.IsChecked == true)
            {
                CheckMoveGrid.IsChecked = false;
                return;
            }

            /* Do nothing if we're using the radial menu */
            if (Keyboard.IsKeyDown(Key.Space)) return;

            /* Do nothing if we're selecting nodes */
            if (Keyboard.IsKeyDown(Key.LeftCtrl)) return;

            /* Get cursor position */
            var curPos = GetCurPos(info, e);

            /* Validate the new camera */
            if (CheckAddCamera.IsChecked == true && _curCameraPerspective != null)
                ValidateNewCamera(info, curPos);

            /* Do nothing if we're manipulating nodes */
            if (SelNodesPerspective.Points.Count > 0)
            {
                /* Use Shift to duplicate */
                if (Keyboard.IsKeyDown(Key.LeftShift))
                {
                    AddNodes(_originalNodes);
                }
                return;
            }

            /* Place a node if adding nodes is enabled */
            if (CheckAddNode.IsChecked == true)
                PlaceNode(info, curPos);

            /* Place a beam */
            if (CheckAddBeam.IsChecked == true)
                PlaceBeam(info, curPos);

            /* Validate the new thruster */
            if (CheckAddThruster.IsChecked == true && _curThrusterPerspective != null)
                ValidateNewThruster(info, curPos);

            /* Place a triangle */
            if (CheckAddTriangle.IsChecked == true)
            {
                /* Add new */
                if (!Keyboard.IsKeyDown(Key.LeftAlt))
                {
                    if (!_triPoint1.HasValue)
                        _triPoint1 = GetAimPoint(info, curPos);
                    else if (!_triPoint2.HasValue)
                        _triPoint2 = GetAimPoint(info, curPos);
                    else
                        PlaceTriangle(info, curPos);
                }

                /* Remove */
                else
                {
                    _triPoint1 = null;
                    _triPoint2 = null;

                    int index1, index2, index3;
                    GetHitTriangle(info, curPos, out index1, out index2, out index3);
                    if (index1 < 0 || index2 < 0 || index3 < 0) return;
                    RemoveTriangle(index1, index2, index3);
                }
            }
        }

        #endregion

        #region Cursor related

        private Point GetCurPos(ViewInfo info, MouseEventArgs e)
        {
            /* 2D cursor position relative to the viewport */
            return e.GetPosition(info.View);
        }

        private Point3D? GetAimPoint(ViewInfo info, Point curPos)
        {
            return
                info.View != PerspectiveViewport ||
                (CheckNodeCursor.IsChecked == false && Check3DCursor.IsChecked == false)
                    ? GetGridPoint(info, curPos)
                    : Check3DCursor.IsChecked == true
                    ? SnapPoint(_currentPosition, SnapValue, PerspectiveGrid.Center)
                        : GetNearestNode(info, CursorPosition);// GetNearestPoint(info.Nodes.Points, CursorPosition);
        }

        private Point3D? GetNearestNode(ViewInfo info, Point3D position)
        {
            return GetNearestPoint(
                info.View.Children.Where(obj => obj is PointsVisual3D && obj != RefPerspective && obj != SelNodesPerspective).Aggregate(new List<Point3D>(), (list, cur) =>
                {
                    list.AddRange(((PointsVisual3D)cur).Points);
                    return list;
                }), position);
        }

        private Point3D? SetCurPoint(ViewInfo info, Point curPos)
        {
            /* Gets the 3D cursor position */
            var curPoint = GetAimPoint(info, curPos);
            if (!curPoint.HasValue) return null;

            /* Show the 3D cursor */
            if (CheckShow3DCursor.IsChecked == true)
                SetCursorPoint(curPoint.Value);
            return curPoint;
        }

        private void SetCursorPoint(Point3D point)
        {
            /* Create a new point collection and use the given point */
            var points = new Point3DCollection(1) { point };

            /* Reset reference points */
            RefPerspective.Points = points;
            RefTop.Points = points;
            RefFront.Points = points;
            RefRight.Points = points;

            /* Change cursor color if we hit a node */
            if (NodesPerspective.Points.Contains(point))
            {
                RefPerspective.Color = (Color)Resources["CursorHitColor"];
                RefTop.Color = (Color)Resources["CursorHitColor"];
                RefFront.Color = (Color)Resources["CursorHitColor"];
                RefRight.Color = (Color)Resources["CursorHitColor"];

                /* Update tooltip */
                if (QueryNodeInfo != null && CheckShowInfo.IsChecked == true)
                    QueryNodeInfo(this, point);
            }
            else
            {
                RefPerspective.Color = (Color)Resources["CursorColor"];
                RefTop.Color = (Color)Resources["CursorColor"];
                RefFront.Color = (Color)Resources["CursorColor"];
                RefRight.Color = (Color)Resources["CursorColor"];
            }

            /* Save 3D cursor for other uses */
            _cursorPoint = point;
        }

        private void ClearCursor()
        {
            RefPerspective.Points.Clear();
            RefTop.Points.Clear();
            RefFront.Points.Clear();
            RefRight.Points.Clear();
        }

        private void RefreshRef()
        {
            if (RefPerspective == null) return;
            var points = new Point3DCollection(RefPerspective.Points ?? new List<Point3D>());

            RefPerspective.Size = SliderNodeSize.Value + 2;
            RefTop.Size = SliderNodeSize.Value + 2;
            RefFront.Size = SliderNodeSize.Value + 2;
            RefRight.Size = SliderNodeSize.Value + 2;

            RefPerspective.Points = points;
            RefTop.Points = points;
            RefFront.Points = points;
            RefRight.Points = points;
        }

        #endregion

        #region Lights related

        private void MoveLight(ViewInfo info, Point curPos)
        {
            /* Gets the 3D cursor position */
            var curPoint = GetAimPoint(info, curPos);
            if (!curPoint.HasValue) return;

            Light.Position = curPoint.Value;
            UpdateLightSphere();
        }

        private void UpdateLightSphere()
        {
            LightSphere.Transform = new TranslateTransform3D(Light.Position.ToVector3D());
        }

        private void ResetLight()
        {
            CheckMoveLight.IsChecked = false;
            Light.Position = new Point3D(0, 0, 50);
            UpdateLightSphere();
        }

        #endregion

        #region Grid related

        private void MoveGrid(ViewInfo info, Point curPos)
        {
            /* Gets the 3D cursor position */
            var curPoint = GetAimPoint(info, curPos);
            if (!curPoint.HasValue) return;

            _gridTranslation = new TranslateTransform3D(curPoint.Value.ToVector3D()).Value;
            _gridRotation = Matrix3D.Identity;
            if (Keyboard.IsKeyDown(Key.LeftShift))
            {
                ClearCursor();
                var n = GetHitModelOrTriangleNormal(info, curPos) ?? info.Grid.Normal;
                if (n == info.Grid.Normal) return;
                _gridRotation = RotationFromDirection(n, new Vector3D(0, 0, 1), out _gridLengthVector, out _gridWidthVector, out _gridUpVector);
            }

            /* Transform the grid */
            PerspectiveGrid.Transform = new MatrixTransform3D(_gridRotation * _gridTranslation);
            TopGrid.Transform = new MatrixTransform3D(_gridTranslation);
            FrontGrid.Transform = new MatrixTransform3D(_gridTranslation);
            RightGrid.Transform = new MatrixTransform3D(_gridTranslation);
        }

        private void ResetGrid()
        {
            CheckMoveGrid.IsChecked = false;
            PerspectiveGrid.Transform = Transform3D.Identity;
            TopGrid.Transform = Transform3D.Identity;
            FrontGrid.Transform = Transform3D.Identity;
            RightGrid.Transform = Transform3D.Identity;
            _gridTranslation = Matrix3D.Identity;
            _gridRotation = Matrix3D.Identity;
        }

        private void SetGridVisible(bool value)
        {
            /* Prevent crash on load */
            if (PerspectiveGrid == null) return;

            PerspectiveGrid.Visibility = value ? System.Windows.Visibility.Visible : System.Windows.Visibility.Hidden;
            TopGrid.Visibility = value ? System.Windows.Visibility.Visible : System.Windows.Visibility.Hidden;
            FrontGrid.Visibility = value ? System.Windows.Visibility.Visible : System.Windows.Visibility.Hidden;
            RightGrid.Visibility = value ? System.Windows.Visibility.Visible : System.Windows.Visibility.Hidden;

            if (!value)
            {
                //PerspectiveGrid.Transform = new ScaleTransform3D(0.01, 0.01, 0.01);
                //TopGrid.Transform = new ScaleTransform3D(0.01, 0.01, 0.01);
                //FrontGrid.Transform = new ScaleTransform3D(0.01, 0.01, 0.01);
                //RightGrid.Transform = new ScaleTransform3D(0.01, 0.01, 0.01);

            }
            else
            {
                //ResetGrid();
            }
        }

        #endregion

        #region Node related

        private Point3D? PlaceNode(ViewInfo info, Point curPos)
        {
            /* Use left alt to remove */
            var remove = Keyboard.IsKeyDown(Key.LeftAlt);
            var move = Keyboard.IsKeyDown(Key.LeftShift);

            /* Get the nearest snapped point on the grid */
            var point = GetAimPoint(info, curPos);
            if (!point.HasValue) return null;

            /* Currently moving a node */
            if (_movingNode.HasValue)
            {
                AddNode(point.Value);
                UpdateBeamLinks(_movingNode.Value, point.Value);
                UpdateLinkedTriangles(_movingNode.Value, point.Value);
                RemoveNode(_movingNode.Value);
                _movingNode = null;
                return null;
            }

            /* Move node */
            if (!remove && move && NodesPerspective.Points.Contains(point.Value))
            {
                _movingNode = point;
                return null;
            }

            /* Add new node */
            if (!remove) AddNode(point.Value);

            /* Remove node */
            else if (Mouse.LeftButton == MouseButtonState.Pressed) RemoveNode(point.Value);

            /* Return the added node */
            return point;
        }

        private void TransformNodes(Transform3D transform)
        {
            var moved = new List<Tuple<Point3D, Point3D>>();

            var points = new Point3DCollection(NodesPerspective.Points);
            for (var i = 0; i < points.Count; i++)
                if (_selectionNodeIndices.Contains(i))
                {
                    var oldPos = _originalNodes[i];
                    var curPos = points[i];
                    var newPos = SnapPoint(transform.Transform(oldPos), SnapValue, PerspectiveGrid.Center);

                    if (!newPos.Equals(curPos))
                    {
                        points[i] = newPos;

                        UpdateBeamLinks(curPos, newPos);
                        UpdateLinkedTriangles(curPos, newPos);

                        moved.Add(new Tuple<Point3D, Point3D>(curPos, newPos));
                    }
                }

            NodesPerspective.Points = points;
            NodesTop.Points = points;
            NodesFront.Points = points;
            NodesRight.Points = points;

            if (NodesMoved != null)
                NodesMoved(this, moved);
        }

        private void SnapAllNodes()
        {
            var moved = new List<Tuple<Point3D, Point3D>>();

            var points = new Point3DCollection(NodesPerspective.Points);
            for (var i = 0; i < points.Count; i++)
            {
                var oldNode = points[i];
                var newNode = SnapPoint(oldNode, SnapValue);

                if (!newNode.Equals(oldNode))
                {
                    points[i] = newNode;

                    UpdateBeamLinks(oldNode, newNode);
                    UpdateLinkedTriangles(oldNode, newNode);

                    moved.Add(new Tuple<Point3D, Point3D>(oldNode, newNode));
                }
            }
            UpdateTriangleNormals();

            NodesPerspective.Points = points;
            NodesTop.Points = points;
            NodesFront.Points = points;
            NodesRight.Points = points;

            if (NodesMoved != null)
                NodesMoved(this, moved);
        }

        private void SelectNode(Point3D node)
        {
            /* Node already belongs to selection */
            if (SelNodesPerspective.Points.Contains(node))
                return;

            /* Add to selection */
            _selectionNodeIndices.Add(NodesPerspective.Points.IndexOf(node));

            /* Save original node locations */
            if (_originalNodes.Count == 0)
            {
                foreach (var pos in NodesPerspective.Points)
                    _originalNodes.Add(pos);
            }

            /* Add the node to the selection */
            var points = new Point3DCollection(SelNodesPerspective.Points) { node };
            SelNodesPerspective.Points = points;
            SelNodesTop.Points = points;
            SelNodesFront.Points = points;
            SelNodesRight.Points = points;

            /* Update selection info */
            SelectionCount = points.Count;
            IsSelectionActive = true;

            /* Rebind manipulator */
            UnBindManipulator(Manipulator);
            BindManipulator(Manipulator, SelNodesPerspective);

            /* Also remove the last shown selected triangle if needed */
            ClearHitTriangle();
        }

        private void UnSelectNode(Point3D node)
        {
            /* Node doesn't belong to selection */
            if (!SelNodesPerspective.Points.Contains(node))
                return;

            /* Remove from selection */
            var points = new Point3DCollection(SelNodesPerspective.Points);
            points.Remove(node);
            SelNodesPerspective.Points = points;
            SelNodesTop.Points = points;
            SelNodesFront.Points = points;
            SelNodesRight.Points = points;
        }

        private void RefreshNodes()
        {
            if (NodesPerspective == null) return;
            var points = new Point3DCollection(NodesPerspective.Points ?? new List<Point3D>());

            NodesPerspective.Size = SliderNodeSize.Value;
            NodesTop.Size = SliderNodeSize.Value;
            NodesFront.Size = SliderNodeSize.Value;
            NodesRight.Size = SliderNodeSize.Value;

            NodesPerspective.Points = points;
            NodesTop.Points = points;
            NodesFront.Points = points;
            NodesRight.Points = points;
        }

        #endregion

        #region Beam related

        private Point3D? PlaceBeam(ViewInfo info, Point curPos)
        {
            /* Left alt means remove */
            if (Keyboard.IsKeyDown(Key.LeftAlt))
            {
                _lineStart = null;
                if (HitBeamPerspective.Points.Count == 0) return null;
                RemoveBeam(HitBeamPerspective.Points[0], HitBeamPerspective.Points[1]);
                return null;
            }

            /* No start point defined */
            if (!_lineStart.HasValue) return null;

            /* Get and reset beam start */
            var start = _lineStart;
            _lineStart = null;

            /* Do not add beam if we're moving a node */
            if (_movingNode.HasValue) return null;

            /* Get the nearest snapped point on the grid */
            var end = GetAimPoint(info, curPos);
            if (!end.HasValue) return null;

            /* Don't place beams with a single node */
            if (start.Value == end.Value) return null;

            /* Ensure that both nodes exist */
            //if (!NodesPerspective.Points.Contains(start.Value) ||
            //    !NodesPerspective.Points.Contains(end.Value))
            //{ return null; }

            /* Add beam */
            AddBeam(start.Value, end.Value);

            /* Return the end point */
            return end;
        }

        private void RemoveLinkedBeams(Point3D node)
        {
            /* Node is not used by beams */
            if (!BeamsPerspective.Points.Contains(node)) return;

            /* Create a new beams collection */
            var points = new Point3DCollection(BeamsPerspective.Points);

            var appearances = points.Count(p => p.Equals(node));

            /* Remove all pairs containing the given node */
            for (var bid = 0; bid < points.Count; bid += 2)
            {
                var pt1 = points[bid];
                var pt2 = points[bid + 1];

                if (!pt1.Equals(node) && !pt2.Equals(node)) continue;

                points.RemoveAt(bid);
                points.RemoveAt(bid);

                /* Notify */
                if (BeamRemoved != null)
                    BeamRemoved(this, new Tuple<Point3D, Point3D>(pt1, pt2));

                bid -= 2;
            }

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;

            /* Notify */
            if (BeamsRemoved != null)
                BeamsRemoved(this, node);
        }

        private void UpdateBeamLinks(Point3D oldNode, Point3D newNode)
        {
            /* Node is not used by beams */
            if (!BeamsPerspective.Points.Contains(oldNode)) return;

            /* Create a new beams collection */
            var points = new Point3DCollection(BeamsPerspective.Points);

            /* Replace nodes */
            for (var bid = 0; bid < points.Count; bid += 2)
            {
                var pt1 = points[bid];
                var pt2 = points[bid + 1];

                if (pt1.Equals(oldNode))
                    points[bid] = newNode;

                if (pt2.Equals(oldNode))
                    points[bid + 1] = newNode;
            }

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;
        }

        private void DuplicateBeamLinks(IEnumerable<Tuple<Point3D, Point3D>> twins)
        {
            /* Create a new beams collection */
            var points = new Point3DCollection(BeamsPerspective.Points);

            foreach (var twin in twins)
            {
                /* Node is not used by beams */
                if (!BeamsPerspective.Points.Contains(twin.Item1)) continue;

                /* Replace nodes */
                for (var bid = 0; bid < points.Count; bid += 2)
                {
                    var pt1 = points[bid];
                    var pt2 = points[bid + 1];

                    if (pt1.Equals(twin.Item1))
                    {
                        AddBeam(twin.Item2, pt2);
                    }

                    if (pt2.Equals(twin.Item1))
                    {
                        AddBeam(pt1, twin.Item2);
                    }
                }
            }
        }

        private void SelectLinkedBeams(Point3D node)
        {
            /* Node is not used by beams */
            if (!BeamsPerspective.Points.Contains(node)) return;

            /* Create a new beams collection */
            var points = new Point3DCollection(BeamsPerspective.Points);
            var selPoints = new Point3DCollection(SelBeamsPerspective.Points);

            for (var bid = 0; bid < points.Count; bid += 2)
            {
                var pt1 = points[bid];
                var pt2 = points[bid + 1];

                if (pt1.Equals(node))
                {
                    points.Remove(pt1);
                    selPoints.Add(pt1);
                }

                if (pt2.Equals(node))
                {
                    points.Remove(pt2);
                    selPoints.Add(pt2);
                }

                bid -= 2;
            }

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;
            SelBeamsPerspective.Points = selPoints;
            SelBeamsTop.Points = selPoints;
            SelBeamsFront.Points = points;
            SelBeamsRight.Points = points;
        }

        private void UnSelectLinkedBeams(Point3D node)
        {
            /* Node is not used by beams */
            if (!SelBeamsPerspective.Points.Contains(node)) return;

            /* Create a new beams collection */
            var points = new Point3DCollection(BeamsPerspective.Points);
            var selPoints = new Point3DCollection(SelBeamsPerspective.Points);

            /* Remove all pairs containing the given node from selection */
            /* Add them to normal list */
            for (var bid = 0; bid < selPoints.Count; bid += 2)
            {
                var pt1 = selPoints[bid];
                var pt2 = selPoints[bid + 1];

                if (!pt1.Equals(node) && !pt2.Equals(node)) continue;

                points.Add(pt1);
                points.Add(pt2);

                selPoints.RemoveAt(bid);
                selPoints.RemoveAt(bid);

                bid -= 2;
            }

            /* Reset beams */
            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;
            SelBeamsPerspective.Points = selPoints;
            SelBeamsTop.Points = selPoints;
            SelBeamsFront.Points = points;
            SelBeamsRight.Points = points;
        }

        private void RefreshBeams()
        {
            if (BeamsPerspective == null) return;
            var points = new Point3DCollection(BeamsPerspective.Points ?? new List<Point3D>());

            BeamsPerspective.Thickness = SliderBeamThickness.Value;
            BeamsTop.Thickness = SliderBeamThickness.Value;
            BeamsFront.Thickness = SliderBeamThickness.Value;
            BeamsRight.Thickness = SliderBeamThickness.Value;

            BeamsPerspective.Points = points;
            BeamsTop.Points = points;
            BeamsFront.Points = points;
            BeamsRight.Points = points;
        }

        private void RefreshHitBeam()
        {
            if (HitBeamPerspective == null) return;
            var points = new Point3DCollection(HitBeamPerspective.Points ?? new List<Point3D>());

            HitBeamPerspective.Thickness = SliderBeamThickness.Value;
            HitBeamTop.Thickness = SliderBeamThickness.Value;
            HitBeamFront.Thickness = SliderBeamThickness.Value;
            HitBeamRight.Thickness = SliderBeamThickness.Value;

            HitBeamPerspective.Points = points;
            HitBeamTop.Points = points;
            HitBeamFront.Points = points;
            HitBeamRight.Points = points;
        }

        private void ClearHitBeam()
        {
            HitBeamPerspective.Points = new Point3DCollection();
            HitBeamTop.Points = new Point3DCollection();
            HitBeamFront.Points = new Point3DCollection();
            HitBeamRight.Points = new Point3DCollection();
        }

        private void UpdateHitBeam(ViewInfo info, Point curPos)
        {
            ClearHitBeam();

            /* Get hit triangle */
            Point3D? node1, node2;
            GetHitBeam(info, curPos, out node1, out node2);
            if (node1 == null || node2 == null) return;

            var points = new Point3DCollection { node1.Value, node2.Value };

            /* Show beam */
            HitBeamPerspective.Points = points;
            HitBeamTop.Points = points;
            HitBeamFront.Points = points;
            HitBeamRight.Points = points;

            /* Show tooltip */
            if (QueryBeamsInfo != null && CheckShowInfo.IsChecked == true)
                QueryBeamsInfo(this, new Tuple<Point3D, Point3D>(node1.Value, node2.Value));
        }

        private void RemoveHitBeam()
        {
            if (HitBeamPerspective.Points.Count() < 2) return;

            RemoveBeam(HitBeamPerspective.Points[0], HitBeamPerspective.Points[1]);
            ClearHitBeam();
        }

        private bool BeamExists(Point3D node1, Point3D node2)
        {
            /* Either of the nodes is not used by beams */
            var points = BeamsPerspective.Points;
            if (!points.Contains(node1) || !points.Contains(node2))
                return false;

            /* Check beam points */
            for (var bid = 0; bid < points.Count; bid += 2)
            {
                var pt1 = points[bid];
                var pt2 = points[bid + 1];

                if (pt1.Equals(node1) && pt2.Equals(node2))
                    return true;

                if (pt2.Equals(node1) && pt1.Equals(node2))
                    return true;
            }

            return false;
        }

        #endregion

        #region Triangle related

        private Point3D? PlaceTriangle(ViewInfo info, Point curPos)
        {
            /* Do nothing if we're moving a node */
            if (_movingNode.HasValue) return null;

            /* Use left alt to remove */
            var remove = Keyboard.IsKeyDown(Key.LeftAlt);

            /* First points need to be defined */
            if (!_triPoint1.HasValue || !_triPoint2.HasValue) return null;

            /* Get last point */
            var triPoint1 = _triPoint1.Value;
            var triPoint2 = _triPoint2.Value;
            var triPoint3 = GetAimPoint(info, curPos);
            if (!triPoint3.HasValue) return null;

            /* Reset first points */
            _triPoint1 = null;
            _triPoint2 = null;

            /* Ensure that all nodes exist */
            //if (!NodesPerspective.Points.Contains(triPoint1) ||
            //    !NodesPerspective.Points.Contains(triPoint2) ||
            //    !NodesPerspective.Points.Contains(triPoint3.Value))
            //{ return null; }

            /* Add a new triangle */
            if (!remove) AddTriangle(triPoint1, triPoint2, triPoint3.Value);

            /* Return the last point */
            return triPoint3;
        }

        private void RemoveLinkedTriangles(Point3D node)
        {
            /* Node is not used by triangles */
            if (!TriangleGeometryPerspective.Positions.Contains(node)) return;

            /* Create new collections */
            var positions = new Point3DCollection(TriangleGeometryPerspective.Positions);
            var normals = new Vector3DCollection(TriangleGeometryPerspective.Normals);
            var indices = new Int32Collection();

            /* Remove all triangles associated with the given node */
            for (var tid = 0; tid < positions.Count; tid += 3)
            {
                var pt1 = positions[tid];
                var pt2 = positions[tid + 1];
                var pt3 = positions[tid + 2];

                if (!pt1.Equals(node) && !pt2.Equals(node) && !pt3.Equals(node)) continue;

                positions.RemoveAt(tid);
                positions.RemoveAt(tid);
                positions.RemoveAt(tid);

                normals.RemoveAt(tid);
                normals.RemoveAt(tid);
                normals.RemoveAt(tid);

                /* Notify */
                if (TriangleRemoved != null)
                    TriangleRemoved(this, new Tuple<Point3D, Point3D, Point3D>(pt1, pt2, pt3));

                tid -= 3;
            }

            /* Update indices */
            for (var i = 0; i < positions.Count; i++)
                indices.Add(i);

            /* Reset triangles */
            TriangleGeometryPerspective.Positions = positions;
            TriangleGeometryPerspective.Normals = normals;
            TriangleGeometryPerspective.TriangleIndices = indices;
            TriangleGeometryTop.Positions = positions;
            TriangleGeometryTop.Normals = normals;
            TriangleGeometryTop.TriangleIndices = indices;
            TriangleGeometryFront.Positions = positions;
            TriangleGeometryFront.Normals = normals;
            TriangleGeometryFront.TriangleIndices = indices;
            TriangleGeometryRight.Positions = positions;
            TriangleGeometryRight.Normals = normals;
            TriangleGeometryRight.TriangleIndices = indices;

            /* Update triangle normals */
            UpdateTriangleNormals();

            /* Update visual normals */
            if (CheckShowNormals.IsChecked == true)
                UpdateNormals();

            /* Notify */
            if (TrianglesRemoved != null)
                TrianglesRemoved(this, node);
        }

        private void UpdateLinkedTriangles(Point3D oldNode, Point3D newNode)
        {
            /* Node is not used by triangles */
            if (!TriangleGeometryPerspective.Positions.Contains(oldNode)) return;

            /* Update positions */
            TriangleGeometryPerspective.Positions = new Point3DCollection(TriangleGeometryPerspective.Positions.Select(pos => pos.Equals(oldNode) ? newNode : pos));
            TriangleGeometryTop.Positions = new Point3DCollection(TriangleGeometryTop.Positions.Select(pos => pos.Equals(oldNode) ? newNode : pos));
            TriangleGeometryFront.Positions = new Point3DCollection(TriangleGeometryFront.Positions.Select(pos => pos.Equals(oldNode) ? newNode : pos));
            TriangleGeometryRight.Positions = new Point3DCollection(TriangleGeometryRight.Positions.Select(pos => pos.Equals(oldNode) ? newNode : pos));

            /* Update triangle normals */
            UpdateTriangleNormals();

            /* Update visual normals */
            if (CheckShowNormals.IsChecked == true)
                UpdateNormals();
        }

        private void UpdateTriangleNormals()
        {
            var curPositions = TriangleGeometryPerspective.Positions;
            var newNormals = new Vector3DCollection();
            for (var i = 0; i < curPositions.Count; i += 3)
            {
                var n = CalculateTriangleNormal(curPositions[i], curPositions[i + 1], curPositions[i + 2]);
                newNormals.Add(n);
                newNormals.Add(n);
                newNormals.Add(n);
            }

            TriangleGeometryPerspective.Normals = newNormals;
            TriangleGeometryTop.Normals = newNormals;
            TriangleGeometryFront.Normals = newNormals;
            TriangleGeometryRight.Normals = newNormals;
        }

        private void ClearHitTriangle()
        {
            HitTriangleGeometryPerspective.Positions.Clear();
            HitTriangleGeometryPerspective.TriangleIndices.Clear();
            HitTriangleGeometryPerspective.Normals.Clear();

            HitTriangleGeometryTop.Positions.Clear();
            HitTriangleGeometryTop.TriangleIndices.Clear();
            HitTriangleGeometryTop.Normals.Clear();

            HitTriangleGeometryFront.Positions.Clear();
            HitTriangleGeometryFront.TriangleIndices.Clear();
            HitTriangleGeometryFront.Normals.Clear();

            HitTriangleGeometryRight.Positions.Clear();
            HitTriangleGeometryRight.TriangleIndices.Clear();
            HitTriangleGeometryRight.Normals.Clear();
        }

        private void UpdateHitTriangle(ViewInfo info, Point curPos)
        {
            ClearHitTriangle();

            /* Get hit triangle */
            int index1, index2, index3;
            GetHitTriangle(info, curPos, out index1, out index2, out index3);
            if (index1 < 0 || index2 < 0 || index3 < 0) return;

            /* Update global variables */
            _triIndex1 = index1;
            _triIndex2 = index2;
            _triIndex3 = index3;

            /* Points */
            var point1 = TriangleGeometryPerspective.Positions[index1];
            var point2 = TriangleGeometryPerspective.Positions[index2];
            var point3 = TriangleGeometryPerspective.Positions[index3];

            /* Define lists */
            var normal = TriangleGeometryPerspective.Normals[index1];
            var normals = new Vector3DCollection() { normal, normal, normal };
            var positions = new Point3DCollection()
            {
                point1,point2,point3
                //Point3D.Add(point1, normal / 10.0),
                //Point3D.Add(point2, normal / 10.0),
                //Point3D.Add(point3, normal / 10.0)
            };
            var indices = new Int32Collection() { 0, 1, 2 };

            /* Show triangle */
            HitTriangleGeometryPerspective.Positions = positions;
            HitTriangleGeometryPerspective.Normals = normals;
            HitTriangleGeometryPerspective.TriangleIndices = indices;

            HitTriangleGeometryTop.Positions = positions;
            HitTriangleGeometryTop.Normals = normals;
            HitTriangleGeometryTop.TriangleIndices = indices;

            HitTriangleGeometryFront.Positions = positions;
            HitTriangleGeometryFront.Normals = normals;
            HitTriangleGeometryFront.TriangleIndices = indices;

            HitTriangleGeometryRight.Positions = positions;
            HitTriangleGeometryRight.Normals = normals;
            HitTriangleGeometryRight.TriangleIndices = indices;

            /* Show tooltip */
            if (QueryTriangleInfo != null && CheckShowInfo.IsChecked == true)
                QueryTriangleInfo(this, new Tuple<Point3D, Point3D, Point3D>(
                    TriangleGeometryPerspective.Positions[index1],
                    TriangleGeometryPerspective.Positions[index2],
                    TriangleGeometryPerspective.Positions[index3]));
        }

        private void RemoveHitTriangle()
        {
            if (!HitTriangleGeometryPerspective.Positions.Any()) return;

            RemoveTriangle(_triIndex1, _triIndex2, _triIndex3);

            ClearHitTriangle();
        }

        #endregion

        #region Thruster related

        /// <summary>Places a new thruster</summary>
        /// <param name="info"></param>
        /// <param name="curPos"></param>
        /// <returns></returns>
        private void PlaceThruster(ViewInfo info, Point curPos)
        {
            /* Get the starting position */
            var startPos = CheckNodeCursor.IsChecked == true || Check3DCursor.IsChecked == true ? _cursorPoint : GetAimPoint(info, curPos);
            if (!startPos.HasValue) return;

            /* Define the temporary position */
            var endPos = Point3D.Add(startPos.Value, new Vector3D(0.01, 0, 0));

            /* Create a new pipe visual based on the template */
            _curThrusterPerspective = ClonePipe(ThrusterTemplatePerspective, startPos.Value, endPos);
            _curThrusterTop = ClonePipe(ThrusterTemplatePerspective, startPos.Value, endPos);
            _curThrusterFront = ClonePipe(ThrusterTemplatePerspective, startPos.Value, endPos);
            _curThrusterRight = ClonePipe(ThrusterTemplatePerspective, startPos.Value, endPos);

            /* Add it to the viewports */
            PerspectiveViewport.Children.Insert(
                PerspectiveViewport.Children.IndexOf(BeamsPerspective) + 1,
                _curThrusterPerspective
            );
            TopViewport.Children.Insert(
                TopViewport.Children.IndexOf(BeamsTop) + 1,
                _curThrusterTop
            );
            FrontViewport.Children.Insert(
                FrontViewport.Children.IndexOf(BeamsFront) + 1,
                _curThrusterFront
            );
            RightViewport.Children.Insert(
                RightViewport.Children.IndexOf(BeamsRight) + 1,
                _curThrusterRight
            );
        }

        /// <summary>Updates the thruster currently being added</summary>
        /// <param name="info"></param>
        /// <param name="curPos"></param>
        private Point3D? UpdateNewThruster(ViewInfo info, Point curPos)
        {
            /* No thruster being added */
            if (_curThrusterPerspective == null) return null;

            /* Calculate new end pos */
            var endPosN = GetAimPoint(info, curPos);
            if (!endPosN.HasValue) return null;

            /* Update the thruster */
            var startPos = _curThrusterPerspective.Point1;
            var endPos =
                !endPosN.Value.Equals(startPos)
                ? endPosN.Value
                : Point3D.Add(endPosN.Value, new Vector3D(0.01, 0, 0));
            _curThrusterPerspective.Point2 = endPos;
            _curThrusterTop.Point2 = endPos;
            _curThrusterFront.Point2 = endPos;
            _curThrusterRight.Point2 = endPos;

            return endPos;
        }

        /// <summary>Validates the newly created thruster</summary>
        /// <param name="info"></param>
        /// <param name="curPos"></param>
        private void ValidateNewThruster(ViewInfo info, Point curPos)
        {
            /* No thruster being added */
            if (_curThrusterPerspective == null) return;

            /* Update it */
            var endPos = UpdateNewThruster(info, curPos);

            /* Reset the temp variable */
            var startPos = _curThrusterPerspective.Point1;
            _curThrusterPerspective = null;
            _curThrusterTop = null;
            _curThrusterFront = null;
            _curThrusterRight = null;

            /* Notify */
            if (ThrusterAdded != null)
                ThrusterAdded(this, new Tuple<Point3D, Point3D>(startPos, endPos.Value));
        }

        /// <summary>Shows or hides the thrusters</summary>
        /// <param name="value"></param>
        private void SetThrustersVisible(bool value)
        {
            if (PerspectiveViewport == null) return;

            foreach (var thruster in PerspectiveViewport.Children.Where(child => child is PipeVisual3D && child != ThrusterTemplatePerspective).Cast<PipeVisual3D>())
                thruster.Visible = value;
            foreach (var thruster in TopViewport.Children.Where(child => child is PipeVisual3D).Cast<PipeVisual3D>())
                thruster.Visible = value;
            foreach (var thruster in FrontViewport.Children.Where(child => child is PipeVisual3D).Cast<PipeVisual3D>())
                thruster.Visible = value;
            foreach (var thruster in RightViewport.Children.Where(child => child is PipeVisual3D).Cast<PipeVisual3D>())
                thruster.Visible = value;
        }

        #endregion

        #region Camera related

        /// <summary>Place a new camera</summary>
        /// <param name="info"></param>
        /// <param name="curPos"></param>
        private void PlaceCamera(ViewInfo info, Point curPos)
        {
            /* Get the starting position */
            var startPos = CheckNodeCursor.IsChecked == true || Check3DCursor.IsChecked == true ? _cursorPoint : GetAimPoint(info, curPos);
            if (!startPos.HasValue) return;

            /* Define the temporary position */
            var endPos = Point3D.Add(startPos.Value, new Vector3D(0.01, 0, 0));

            /* Create a new cone visual based on the template */
            _curCameraPerspective = CloneCone(CameraTemplatePerspective, startPos.Value, endPos);
            _curCameraTop = CloneCone(CameraTemplatePerspective, startPos.Value, endPos);
            _curCameraFront = CloneCone(CameraTemplatePerspective, startPos.Value, endPos);
            _curCameraRight = CloneCone(CameraTemplatePerspective, startPos.Value, endPos);

            /* Add it to the viewports */
            PerspectiveViewport.Children.Insert(
                PerspectiveViewport.Children.IndexOf(NodesPerspective) + 1,
                _curCameraPerspective
            );
            TopViewport.Children.Insert(
                TopViewport.Children.IndexOf(NodesTop) + 1,
                _curCameraTop
            );
            FrontViewport.Children.Insert(
                FrontViewport.Children.IndexOf(NodesFront) + 1,
                _curCameraFront
            );
            RightViewport.Children.Insert(
                RightViewport.Children.IndexOf(NodesRight) + 1,
                _curCameraRight
            );
        }

        /// <summary>Updates the camera currently being added</summary>
        /// <param name="info"></param>
        /// <param name="curPos"></param>
        private Point3D? UpdateNewCamera(ViewInfo info, Point curPos)
        {
            /* No camera being added */
            if (_curCameraPerspective == null) return null;

            /* Calculate new end pos */
            var endPosN = GetAimPoint(info, curPos);
            if (!endPosN.HasValue) return null;

            /* Update the camera */
            var startPos = _curCameraPerspective.Origin;
            var endPos =
                !endPosN.Value.Equals(startPos)
                ? endPosN.Value
                : Point3D.Add(endPosN.Value, new Vector3D(0.01, 0, 0));

            _curCameraPerspective.Origin = startPos;
            _curCameraPerspective.Normal = startPos - endPos;
            _curCameraPerspective.Height = (startPos - endPos).Length;

            _curCameraTop.Origin = startPos;
            _curCameraTop.Normal = startPos - endPos;
            _curCameraTop.Height = (startPos - endPos).Length;

            _curCameraFront.Origin = startPos;
            _curCameraFront.Normal = startPos - endPos;
            _curCameraFront.Height = (startPos - endPos).Length;

            _curCameraRight.Origin = startPos;
            _curCameraRight.Normal = startPos - endPos;
            _curCameraRight.Height = (startPos - endPos).Length;

            /* Display FOV info */
            ItemNewCameraFov.Visibility = Visibility.Visible;
            TextNewCameraFov.Text = CalculateConeFOV(_curCameraPerspective).ToStringOrZero();

            return endPos;
        }

        /// <summary>Validates the newly created camera</summary>
        /// <param name="info"></param>
        /// <param name="curPos"></param>
        private void ValidateNewCamera(ViewInfo info, Point curPos)
        {
            /* No camera being added */
            if (_curCameraPerspective == null) return;

            /* Update it */
            var endPos = UpdateNewCamera(info, curPos);
            if (endPos == null) return;

            /* Reset the temp variable */
            var startPos = _curCameraPerspective.Origin;

            /* Calculate FOV */
            var fov = CalculateConeFOV(_curCameraPerspective);

            _curCameraPerspective = null;
            _curCameraTop = null;
            _curCameraFront = null;
            _curCameraRight = null;

            /* Hide FOV info */
            ItemNewCameraFov.Visibility = Visibility.Hidden;

            /* Notify new internal camera */
            if (SelectionCount == 6 && InternalCameraAdded != null)
                InternalCameraAdded(this,
                    new Tuple<Point3D, float, Tuple<Point3D, Point3D, Point3D, Point3D, Point3D, Point3D>>(startPos, fov,
                        new Tuple<Point3D, Point3D, Point3D, Point3D, Point3D, Point3D>(
                            SelNodesPerspective.Points[0], SelNodesPerspective.Points[1], SelNodesPerspective.Points[2],
                            SelNodesPerspective.Points[3], SelNodesPerspective.Points[4], SelNodesPerspective.Points[5])));

            /* Notify change of external camera */
            else if (!IsSelectionActive && ExternalCameraChanged != null)
                ExternalCameraChanged(this, new Tuple<Point3D, float>(startPos, fov));
        }

        /// <summary>Shows or hides the cameras</summary>
        /// <param name="value"></param>
        private void SetCamerasVisible(bool value)
        {
            if (PerspectiveViewport == null) return;

            foreach (var camera in PerspectiveViewport.Children.Where(child => child is TruncatedConeVisual3D && child != CameraTemplatePerspective).Cast<TruncatedConeVisual3D>())
                camera.Visible = value;
            foreach (var camera in TopViewport.Children.Where(child => child is TruncatedConeVisual3D).Cast<TruncatedConeVisual3D>())
                camera.Visible = value;
            foreach (var camera in FrontViewport.Children.Where(child => child is TruncatedConeVisual3D).Cast<TruncatedConeVisual3D>())
                camera.Visible = value;
            foreach (var camera in RightViewport.Children.Where(child => child is TruncatedConeVisual3D).Cast<TruncatedConeVisual3D>())
                camera.Visible = value;

            if (!value)
            {
                PerspectiveViewport.Children.Remove(CameraLinksPerspective);
                TopViewport.Children.Remove(CameraLinksTop);
                FrontViewport.Children.Remove(CameraLinksFront);
                RightViewport.Children.Remove(CameraLinksRight);
            }
            else
            {
                if (!PerspectiveViewport.Children.Contains(CameraLinksPerspective))
                    PerspectiveViewport.Children.Insert(PerspectiveViewport.Children.IndexOf(RefPerspective) + 1, CameraLinksPerspective);
                if (!TopViewport.Children.Contains(CameraLinksTop))
                    TopViewport.Children.Insert(TopViewport.Children.IndexOf(RefTop) + 1, CameraLinksTop);
                if (!FrontViewport.Children.Contains(CameraLinksFront))
                    FrontViewport.Children.Insert(FrontViewport.Children.IndexOf(RefFront) + 1, CameraLinksFront);
                if (!RightViewport.Children.Contains(CameraLinksRight))
                    RightViewport.Children.Insert(RightViewport.Children.IndexOf(RefRight) + 1, CameraLinksRight);
            }
        }

        /// <summary>Removes the visual links for the camera at the specified origin</summary>
        /// <param name="origin"></param>
        private void RemoveCameraLinks(Point3D origin)
        {
            /* Origin is not used by camera */
            if (!CameraLinksPerspective.Points.Contains(origin)) return;

            /* Create a new camera links collection */
            var points = new Point3DCollection(CameraLinksPerspective.Points);

            /* Remove all pairs containing the given position */
            for (var bid = 0; bid < points.Count; bid += 2)
            {
                var pt1 = points[bid];
                var pt2 = points[bid + 1];

                if (!pt1.Equals(origin) && !pt2.Equals(origin)) continue;

                points.RemoveAt(bid);
                points.RemoveAt(bid);

                bid -= 2;
            }

            /* Reset camera links */
            CameraLinksPerspective.Points = points;
            CameraLinksTop.Points = points;
            CameraLinksFront.Points = points;
            CameraLinksRight.Points = points;
        }

        private void ClearCameraLinks()
        {
            /* Already empty */
            if (CameraLinksPerspective.Points.Count == 0) return;

            /* Create a new camera links collection */
            var points = new Point3DCollection();

            /* Reset camera links */
            CameraLinksPerspective.Points = points;
            CameraLinksTop.Points = points;
            CameraLinksFront.Points = points;
            CameraLinksRight.Points = points;
        }

        #endregion

        #region Ghosts related

        private void ShowGhosts(ViewInfo info, Point curPos)
        {
            /* Calculate new ghost */
            var point = GetAimPoint(info, curPos);

            /* Next is for beams only */
            if (CheckAddBeam.IsChecked == false) return;

            /* Reset ghost beam */
            ClearGhosts();
            if (_lineStart.HasValue && point.HasValue)
            {
                AddGhost(_lineStart.Value);
                AddGhost(point.Value);
            }
            _beamGhost = point;
        }

        private void AddGhost(Point3D ghost)
        {
            if (GhostsPerspective.Points.Contains(ghost)) return;
            var points = new Point3DCollection(GhostsPerspective.Points);
            points.Add(ghost);
            GhostsTop.Points = points;
            GhostsFront.Points = points;
            GhostsRight.Points = points;
            GhostsPerspective.Points = points;
        }

        private void ClearGhosts()
        {
            var points = new Point3DCollection();
            GhostsTop.Points = points;
            GhostsFront.Points = points;
            GhostsRight.Points = points;
            GhostsPerspective.Points = points;
        }

        private void RefreshGhosts()
        {
            if (GhostsPerspective == null) return;
            var points = new Point3DCollection(GhostsPerspective.Points ?? new List<Point3D>());

            GhostsPerspective.Thickness = SliderBeamThickness.Value;
            GhostsTop.Thickness = SliderBeamThickness.Value;
            GhostsFront.Thickness = SliderBeamThickness.Value;
            GhostsRight.Thickness = SliderBeamThickness.Value;

            GhostsPerspective.Points = points;
            GhostsTop.Points = points;
            GhostsFront.Points = points;
            GhostsRight.Points = points;
        }

        #endregion

        #region Names related

        private void TransformNames()
        {
            /* Create new names collection */
            var names = new List<BillboardTextItem>(NodeNamesPerspective.Items ?? new List<BillboardTextItem>());

            /* Reposition linked names */
            for (var nid = 0; nid < names.Count; nid += 1)
            {
                if (_selectionNodeIndices.Contains(nid))
                {
                    names[nid].Position = NodesPerspective.Points[nid];
                }
            }

            NodeNamesPerspective.Items = names;
            NodeNamesTop.Items = names;
            NodeNamesFront.Items = names;
            NodeNamesRight.Items = names;
        }

        private void RefreshNames()
        {
            if (NodeNamesPerspective == null) return;
            var names = new List<BillboardTextItem>(NodeNamesPerspective.Items ?? new List<BillboardTextItem>());

            NodeNamesPerspective.PinWidth = SliderPinLength.Value;
            NodeNamesTop.PinWidth = SliderPinLength.Value;
            NodeNamesFront.PinWidth = SliderPinLength.Value;
            NodeNamesRight.PinWidth = SliderPinLength.Value;

            NodeNamesPerspective.Offset = new Vector(SliderPinLength.Value * 2, SliderPinLength.Value * 2);
            NodeNamesTop.Offset = new Vector(SliderPinLength.Value * 2, SliderPinLength.Value * 2);
            NodeNamesFront.Offset = new Vector(SliderPinLength.Value * 2, SliderPinLength.Value * 2);
            NodeNamesRight.Offset = new Vector(SliderPinLength.Value * 2, SliderPinLength.Value * 2);

            NodeNamesPerspective.FontSize = SliderNameSize.Value;
            NodeNamesTop.FontSize = SliderNameSize.Value;
            NodeNamesFront.FontSize = SliderNameSize.Value;
            NodeNamesRight.FontSize = SliderNameSize.Value;

            NodeNamesPerspective.Items = names;
            NodeNamesTop.Items = names;
            NodeNamesFront.Items = names;
            NodeNamesRight.Items = names;
        }

        #endregion

        #region ToolTip related

        private void ShowToolTip(Point3D position, string text)
        {
            ToolTipPerspective.Text = text;
            ToolTipTop.Text = text;
            ToolTipFront.Text = text;
            ToolTipRight.Text = text;

            ToolTipPerspective.Position = position;
            ToolTipTop.Position = position;
            ToolTipFront.Position = position;
            ToolTipRight.Position = position;

            if (PerspectiveViewport.Children.Contains(ToolTipPerspective)) return;
            PerspectiveViewport.Children.Insert(PerspectiveViewport.Children.IndexOf(ModelPerspective) + 1, ToolTipPerspective);
            TopViewport.Children.Insert(TopViewport.Children.IndexOf(ModelTop) + 1, ToolTipTop);
            FrontViewport.Children.Insert(FrontViewport.Children.IndexOf(ModelFront) + 1, ToolTipFront);
            RightViewport.Children.Insert(RightViewport.Children.IndexOf(ModelRight) + 1, ToolTipRight);
        }

        private void ShowOnHUD(string text)
        {
            if (!string.IsNullOrEmpty(text))
                text = string.Format("{0}{0}{1}", Environment.NewLine, text);

            PerspectiveViewLabel.Content = string.Format("Perspective{0}", text);
            TopViewLabel.Content = string.Format("Top{0}", text);
            FrontViewLabel.Content = string.Format("Front{0}", text);
            RightViewLabel.Content = string.Format("Right{0}", text);
        }

        private void MoveToolTip(Point3D position)
        {
            ToolTipPerspective.Position = position;
            ToolTipTop.Position = position;
            ToolTipFront.Position = position;
            ToolTipRight.Position = position;
        }

        private void HideToolTip()
        {
            if (!PerspectiveViewport.Children.Contains(ToolTipPerspective)) return;
            PerspectiveViewport.Children.Remove(ToolTipPerspective);
            TopViewport.Children.Remove(ToolTipTop);
            FrontViewport.Children.Remove(ToolTipFront);
            RightViewport.Children.Remove(ToolTipRight);
        }

        #endregion

        #region Selection

        private void MirrorSelection(bool mirrorX, bool mirrorY, bool mirrorZ)
        {
            if (SelNodesPerspective.Points.Count == 0) return;

            var newNodes = MirrorNodes(mirrorX, mirrorY, mirrorZ);
            MirrorBeams(mirrorX, mirrorY, mirrorZ);

            /* Auto select new nodes */
            if (CheckAutoSelect.IsChecked == true && newNodes != null)
            {
                ClearSelection();
                foreach (var node in newNodes)
                    SelectNode(node);
            }
        }

        private IEnumerable<Point3D> MirrorNodes(bool mirrorX, bool mirrorY, bool mirrorZ)
        {
            if (SelNodesPerspective.Points.Count == 0) return null;

            /* Create mirrored nodes */
            var nodeTwins = (
                from oldPos in NodesPerspective.Points
                where SelNodesPerspective.Points.Contains(oldPos)
                select new Tuple<Point3D, Point3D>(oldPos, MirrorPoint(oldPos, mirrorX, mirrorY, mirrorZ, PerspectiveGrid.Transform))
            ).ToList();

            /* Add new nodes */
            AddNodes(nodeTwins.Select(twin => twin.Item2));

            /* Return the new nodes */
            return nodeTwins.Select(twin => twin.Item2);
        }

        private void MirrorBeams(bool mirrorX, bool mirrorY, bool mirrorZ)
        {
            if (SelNodesPerspective.Points.Count == 0) return;

            var points = new Point3DCollection(BeamsPerspective.Points);
            var sel = SelNodesPerspective.Points;

            for (var bid = 0; bid < points.Count; bid += 2)
            {
                var pt1 = points[bid];
                var pt2 = points[bid + 1];

                /* Skip beams that don't belong to the selection */
                if (!sel.Contains(pt1) && !sel.Contains(pt2)) continue;

                /* Mirror the original points */
                var newPt1 = MirrorPoint(pt1, mirrorX, mirrorY, mirrorZ, PerspectiveGrid.Transform);
                var newPt2 = MirrorPoint(pt2, mirrorX, mirrorY, mirrorZ, PerspectiveGrid.Transform);

                /* Skip existing beams */
                if (BeamExists(newPt1, newPt2)) continue;

                AddBeam(newPt1, newPt2);
            }
        }

        private void DuplicateSelection(bool alongX, bool alongY, bool alongZ)
        {
            if (SelNodesPerspective.Points.Count == 0) return;

            var newNodes = DuplicateNodes(alongX, alongY, alongZ);
            DuplicateBeams(alongX, alongY, alongZ);

            /* Auto select new nodes */
            if (CheckAutoSelect.IsChecked == true && newNodes != null)
            {
                ClearSelection();
                foreach (var node in newNodes)
                    SelectNode(node);
            }
        }

        private IEnumerable<Point3D> DuplicateNodes(bool alongX, bool alongY, bool alongZ)
        {
            if (SelNodesPerspective.Points.Count == 0) return null;

            /* Calculate bounds */
            var points = SelNodesPerspective.Points;
            var deltaX = alongX ? SliderDupX.Value : 0;
            var deltaY = alongY ? SliderDupY.Value : 0;
            var deltaZ = alongZ ? SliderDupZ.Value : 0;
            if (alongX && Math.Abs(deltaX) < double.Epsilon)
            {
                var minX = points.Min(pt => pt.X);
                var maxX = points.Max(pt => pt.X);
                var spaceX = maxX - minX < 0 ? -SnapValue : SnapValue;
                deltaX = spaceX + maxX - minX;
            }
            if (alongY && Math.Abs(deltaY) < double.Epsilon)
            {
                var minY = points.Min(pt => pt.Y);
                var maxY = points.Max(pt => pt.Y);
                var spaceY = maxY - minY < 0 ? -SnapValue : SnapValue;
                deltaY = spaceY + maxY - minY;
            }
            if (alongZ && Math.Abs(deltaZ) < double.Epsilon)
            {
                var minZ = points.Min(pt => pt.Z);
                var maxZ = points.Max(pt => pt.Z);
                var spaceZ = maxZ - minZ < 0 ? -SnapValue : SnapValue;
                deltaZ = spaceZ + maxZ - minZ;
            }

            /* Create mirrored nodes */
            var nodeTwins = (
                from oldPos in NodesPerspective.Points
                where points.Contains(oldPos)
                select new Tuple<Point3D, Point3D>(oldPos, DuplicatePoint(oldPos, deltaX, deltaY, deltaZ, PerspectiveGrid.Transform))
            ).ToList();

            /* Add new nodes */
            AddNodes(nodeTwins.Select(twin => twin.Item2));

            /* Return the new nodes */
            return nodeTwins.Select(twin => twin.Item2);
        }

        private void DuplicateBeams(bool alongX, bool alongY, bool alongZ)
        {
            if (SelNodesPerspective.Points.Count == 0) return;

            var points = new Point3DCollection(BeamsPerspective.Points);
            var sel = SelNodesPerspective.Points;

            /* Calculate bounds */
            var deltaX = alongX ? SliderDupX.Value : 0;
            var deltaY = alongY ? SliderDupY.Value : 0;
            var deltaZ = alongZ ? SliderDupZ.Value : 0;
            if (alongX && Math.Abs(deltaX) < double.Epsilon)
            {
                var minX = sel.Min(pt => pt.X);
                var maxX = sel.Max(pt => pt.X);
                var spaceX = maxX - minX < 0 ? -SnapValue : SnapValue;
                deltaX = spaceX + maxX - minX;
            }
            if (alongY && Math.Abs(deltaY) < double.Epsilon)
            {
                var minY = sel.Min(pt => pt.Y);
                var maxY = sel.Max(pt => pt.Y);
                var spaceY = maxY - minY < 0 ? -SnapValue : SnapValue;
                deltaY = spaceY + maxY - minY;
            }
            if (alongZ && Math.Abs(deltaZ) < double.Epsilon)
            {
                var minZ = sel.Min(pt => pt.Z);
                var maxZ = sel.Max(pt => pt.Z);
                var spaceZ = maxZ - minZ < 0 ? -SnapValue : SnapValue;
                deltaZ = spaceZ + maxZ - minZ;
            }

            /* Duplicate the beams */
            for (var bid = 0; bid < points.Count; bid += 2)
            {
                var pt1 = points[bid];
                var pt2 = points[bid + 1];

                /* Skip beams that don't belong to the selection */
                if (!sel.Contains(pt1) && !sel.Contains(pt2)) continue;

                var newPt1 = DuplicatePoint(pt1, deltaX, deltaY, deltaZ, PerspectiveGrid.Transform);
                var newPt2 = DuplicatePoint(pt2, deltaX, deltaY, deltaZ, PerspectiveGrid.Transform);

                /* If the new nodes weren't added preserve the old ones */
                if (!NodesPerspective.Points.Contains(newPt1))
                    newPt1 = pt1;
                if (!NodesPerspective.Points.Contains(newPt2))
                    newPt2 = pt2;

                /* Skip existing beams */
                if (BeamExists(newPt1, newPt2)) continue;

                /* Add the new beam */
                AddBeam(newPt1, newPt2);
            }
        }

        private void SelectAll()
        {
            foreach (var node in NodesPerspective.Points)
                SelectNode(node);
        }

        private void DeleteSelection()
        {
            RemoveNodes(SelNodesPerspective.Points);
            ClearSelection();
        }

        private void ClearSelection()
        {
            /* Clear selection */
            var points = new Point3DCollection();
            SelNodesPerspective.Points = points;
            SelNodesTop.Points = points;
            SelNodesFront.Points = points;
            SelNodesRight.Points = points;

            _originalNodes.Clear();
            _selectionNodeIndices.Clear();
            UnBindManipulator(Manipulator);

            /* Reset transforms */
            SelNodesPerspective.Transform = MatrixTransform3D.Identity;
            SelNodesTop.Transform = MatrixTransform3D.Identity;
            SelNodesFront.Transform = MatrixTransform3D.Identity;
            SelNodesRight.Transform = MatrixTransform3D.Identity;

            SelectionCount = 0;
            IsSelectionActive = false;
        }

        #endregion

        #region Radial menu

        private void OpenRadialMenu(HelixViewport3D viewport)
        {
            if (viewport == null) return;

            var grid = viewport.Parent as Grid;
            if (grid == null) return;

            var curPos = Mouse.GetPosition(OuterGrid);
            //if (RadialMenu.Parent != null)
            //    ((Grid)RadialMenu.Parent).Children.Remove(RadialMenu);
            //grid.Children.Add(RadialMenu);
            RadialMenu.Visibility = System.Windows.Visibility.Visible;
            RadialMenuTransform.X = curPos.X - RadialMenu.MenuSize / 2;
            RadialMenuTransform.Y = curPos.Y - RadialMenu.MenuSize / 2;
        }

        #endregion

        #endregion

        #region Static methods

        private static void GetHitTriangle(ViewInfo info, Point pos, out int index1, out int index2, out int index3)
        {
            index1 = -1;
            index2 = -1;
            index3 = -1;

            info.View.Viewport.Children.Remove(info.Grid);
            var hits = info.View.Viewport.FindHits(pos).Where(hit => hit.Visual is ModelVisual3D && hit.Visual.IsAncestorOf(info.Triangles));
            info.View.Viewport.Children.Add(info.Grid);
            if (hits == null || !hits.Any()) return;

            var rayHit = hits.ElementAt(0).RayHit;
            index1 = rayHit.VertexIndex1;
            index2 = rayHit.VertexIndex2;
            index3 = rayHit.VertexIndex3;
        }

        private static void GetHitBeam(ViewInfo info, Point pos, out Point3D? node1, out Point3D? node2)
        {
            node1 = null;
            node2 = null;

            info.View.Viewport.Children.Remove(info.Grid);
            var hits = info.View.Viewport.FindHits(pos).Where(hit => hit.Visual is LinesVisual3D && hit.Visual.IsAncestorOf(info.Beams));
            info.View.Viewport.Children.Add(info.Grid);
            if (hits == null || !hits.Any()) return;

            for (var i = 0; i < info.Beams.Points.Count; i += 2)
            {
                var p1 = info.Beams.Points[i];
                var p2 = info.Beams.Points[i + 1];
                var hit = hits.ElementAt(0).RayHit.PointHit;

                var beamDir = SnapPoint(p2, 0) - SnapPoint(p1, 0);
                var hitDir = SnapPoint(hit, 0) - SnapPoint(p1, 0);

                beamDir.Normalize();
                hitDir.Normalize();

                if (!(Math.Abs(beamDir.X - hitDir.X) < 0.01) ||
                    !(Math.Abs(beamDir.Y - hitDir.Y) < 0.01) ||
                    !(Math.Abs(beamDir.Z - hitDir.Z) < 0.01)) continue;

                node1 = p1;
                node2 = p2;
                return;
            }
        }

        private static Vector3D? GetHitModelOrTriangleNormal(ViewInfo info, Point pos)
        {
            info.View.Viewport.Children.Remove(info.Grid);
            var hits = info.View.Viewport.FindHits(pos).Where(hit => hit.Visual is ModelVisual3D && (hit.Visual.IsAncestorOf(info.Model)) || hit.Visual.IsAncestorOf(info.Triangles));
            info.View.Viewport.Children.Add(info.Grid);
            if (hits == null || !hits.Any()) return null;

            var vHit = hits.ElementAt(0);
            return vHit.Normal;
        }

        private static Vector3D? GetHitModelNormal(ViewInfo info, Point pos)
        {
            info.View.Viewport.Children.Remove(info.Grid);
            var hits = info.View.Viewport.FindHits(pos).Where(hit => hit.Visual is ModelVisual3D && hit.Visual.IsAncestorOf(info.Model));
            info.View.Viewport.Children.Add(info.Grid);
            if (hits == null || !hits.Any()) return null;

            return hits.ElementAt(0).Normal;
        }

        private static Vector3D? GetHitTriangleNormal(ViewInfo info, Point pos)
        {
            info.View.Viewport.Children.Remove(info.Grid);
            var hits = info.View.Viewport.FindHits(pos).Where(hit => hit.Visual is ModelVisual3D && hit.Visual.IsAncestorOf(info.Triangles));
            info.View.Viewport.Children.Add(info.Grid);
            if (hits == null || !hits.Any()) return null;

            return hits.ElementAt(0).Normal;
        }

        private Point3D? GetGridPoint(ViewInfo info, Point pos)
        {
            /* Get the closest 3D point on the grid */
            Point3D? point = null;

            if (info.Grid == PerspectiveGrid)
            {
                var offset = new Vector3D(_gridTranslation.OffsetX, _gridTranslation.OffsetY, _gridTranslation.OffsetZ);
                point = info.View.Viewport.UnProject(pos, offset + _gridLengthVector.ToPoint3D(), _gridUpVector);
            }
            else
            {
                point = info.View.Viewport.UnProject(pos, info.Grid.Center.ToVector3D() + info.Grid.LengthDirection.ToPoint3D(), info.Grid.Normal);
            }

            if (!point.HasValue) return null;
            var val = point.Value;

            return SnapToGrid(info, val, info.SnapValue);
        }

        private static Point3D? GetClosestHitNode(ViewInfo info, Point pos)
        {
            /* Get the closest 3D point in the viewport */
            var cam = (PerspectiveCamera)info.View.Camera;
            var point = info.View.Viewport.UnProject(pos, info.Grid.Center.ToVector3D() + cam.Position + cam.LookDirection, cam.LookDirection);
            if (!point.HasValue) return null;
            return GetNearestPoint(info.Nodes.Points, point.Value);
        }

        private static Point3D? GetHitNode(ViewInfo info, Point pos, int power = 2)
        {
            /* Generate some neighbouring points */
            var neighbours = SpiralCoords.GenerateOutTo(power);

            /* Loop potential points */
            foreach (var offset in neighbours)
            {
                var pt = TestHitNode(info, new Point(pos.X + offset.Item1, pos.Y + offset.Item2));
                if (!pt.HasValue) continue;
                return pt;
            }
            return null;
        }

        private static Point3D? GetNearestPoint(IEnumerable<Point3D> list, Point3D point)
        {
            if (!list.Any()) return null;

            return list.Aggregate((cur, next) =>
            {
                double distance;
                cur = GetClosest(point, cur, next, out distance);
                return cur;
            });
        }

        private static Point3D GetClosest(Point3D target, Point3D pt1, Point3D pt2)
        {
            return pt1.DistanceTo(target) < pt2.DistanceTo(target) ? pt1 : pt2;
        }

        private static Point3D GetClosest(Point3D target, Point3D pt1, Point3D pt2, out double distance)
        {
            var closest = pt1.DistanceTo(target) < pt2.DistanceTo(target) ? pt1 : pt2;
            distance = closest.DistanceTo(target);
            return closest;
        }

        private static bool IsPositionOnNode(ViewInfo info, Point3D position)
        {
            return info.Nodes.Points.Contains(position);
        }

        private static Point3D? TestHitNode(ViewInfo info, Point p)
        {
            /* Get viewport hits */
            var hits = info.View.Viewport.FindHits(p);
            if (hits.Count < 1) return null;

            /* Get node hits */
            var nodeHits = hits.Where(hit => hit.Visual == info.Nodes);
            if (!nodeHits.Any()) return null;

            /* Return first node */
            var hitPos = nodeHits.ElementAt(0).Position;

            /* Recalculate node position */
            var x = hitPos.X;
            var y = hitPos.Y;
            var z = hitPos.Z;
            x = SnapToValue(x, info.SnapValue);//, info.Grid.Center.X);
            y = SnapToValue(y, info.SnapValue);//, info.Grid.Center.Y);
            z = SnapToValue(z, info.SnapValue);//, info.Grid.Center.Z);

            /* Ensure the node exists */
            var node = new Point3D(x, y, z);
            if (info.Nodes.Points.Contains(node)) return node;
            return null;
        }

        private static double SnapToValue(double value, double snapValue, double offset = 0)
        {
            if (snapValue < double.Epsilon) snapValue = 0.01;

            snapValue = value > 0 ? snapValue + offset : snapValue - offset;
            var mul = value > 0 ? 1 : -1;
            var val = value + (snapValue * mul) / 2.0;
            return val - (val % snapValue);
        }

        private static Point3D SnapPoint(Point3D point, double snapValue)
        {
            return SnapPoint(point, snapValue, new Point3D(0, 0, 0));
        }

        private static Point3D SnapPoint(Point3D point, double snapValue, Point3D offset)
        {
            return new Point3D(
                SnapToValue(point.X, snapValue, offset.X),
                SnapToValue(point.Y, snapValue, offset.Y),
                SnapToValue(point.Z, snapValue, offset.Z));
        }

        private static Point3D SnapToGrid(GridLines grid, Point3D point, double snapValue)
        {
            var offset = new Vector3D(grid.Transform.Value.OffsetX, grid.Transform.Value.OffsetY, grid.Transform.Value.OffsetZ);
            //var offset = grid.Center;
            var newPoint = new Point3D(point.X - offset.X, point.Y - offset.Y, point.Z - offset.Z);

            var snappedPoint = new Point3D(
                SnapToValue(newPoint.X, snapValue),//, grid.Center.X),
                SnapToValue(newPoint.Y, snapValue),//, grid.Center.Y),
                SnapToValue(newPoint.Z, snapValue));//, grid.Center.Z));
            var finalPoint = new Point3D(snappedPoint.X + offset.X, snappedPoint.Y + offset.Y, snappedPoint.Z + offset.Z);

            return finalPoint;
        }

        private static Point3D SnapToGrid(ViewInfo info, Point3D point, double snapValue)
        {
            return SnapToGrid(info.Grid, point, snapValue);
        }

        private static Point3D Avg(params Point3D[] points)
        {
            return Avg(points.ToList());
        }

        private static Point3D Avg(IList<Point3D> points)
        {
            return new Point3D(
                points.Select(pt => pt.X).Average(),
                points.Select(pt => pt.Y).Average(),
                points.Select(pt => pt.Z).Average()
            );
        }

        private static Vector3D CalculateTriangleNormal(Point3D node1, Point3D node2, Point3D node3)
        {
            var yx = new Vector3D(node2.X - node1.X, node2.Y - node1.Y, node2.Z - node1.Z);
            var yz = new Vector3D(node3.X - node2.X, node3.Y - node2.Y, node3.Z - node2.Z);
            var n = Vector3D.CrossProduct(yx, yz);
            n.Normalize();
            return n;
        }

        private static void RotationVectorsFromDirection(Vector3D direction, Vector3D up, out Vector3D rx, out Vector3D ry, out Vector3D rz)
        {
            rz = direction;
            rz.Normalize();

            ry = Vector3D.CrossProduct(direction, up);
            ry.Normalize();

            rx = Vector3D.CrossProduct(ry, direction);
            rx.Normalize();
        }

        private static Matrix3D RotationFromDirection(Vector3D direction, Vector3D up, out Vector3D rx, out Vector3D ry, out Vector3D rz)
        {
            RotationVectorsFromDirection(direction, up, out rx, out ry, out rz);

            return new Matrix3D(rx.X, rx.Y, rx.Z, 0, ry.X, ry.Y, ry.Z, 0, rz.X, rz.Y, rz.Z, 0, 0, 0, 0, 1);
            //return new Matrix3D(rx.X, ry.X, rz.X, 0, rx.Y, ry.Y, rz.Y, 0, rx.Z, ry.Z, rz.Z, 0, 0, 0, 0, 1);
        }

        private static void BindManipulator(CombinedManipulator manipulator, PointsVisual3D visual)
        {
            manipulator.Position = Avg(visual.Points);
            manipulator.Pivot = manipulator.Position;
            manipulator.Bind(visual);
        }

        private static void UnBindManipulator(CombinedManipulator manipulator)
        {
            manipulator.UnBind();
            manipulator.Position = new Point3D(double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity);
        }

        private static Point3D MirrorPoint(Point3D point, bool mirrorX, bool mirrorY, bool mirrorZ, Transform3D transform = null)
        {
            var tPoint = transform != null && transform.Inverse != null ? transform.Inverse.Transform(point) : point;
            var nPoint = new Point3D(mirrorX ? -tPoint.X : tPoint.X, mirrorY ? -tPoint.Y : tPoint.Y, mirrorZ ? -tPoint.Z : tPoint.Z);
            var fPoint = transform != null && transform.Inverse != null ? transform.Transform(nPoint) : nPoint;
            return SnapPoint(fPoint, 0);
        }

        private static Point3D DuplicatePoint(Point3D point, double alongX, double alongY, double alongZ, Transform3D transform = null)
        {
            var tPoint = transform != null && transform.Inverse != null ? transform.Inverse.Transform(point) : point;
            var nPoint = new Point3D(tPoint.X + alongX, tPoint.Y + alongY, tPoint.Z + alongZ);
            var fPoint = transform != null && transform.Inverse != null ? transform.Transform(nPoint) : nPoint;
            return SnapPoint(fPoint, 0);
        }

        private static PipeVisual3D ClonePipe(PipeVisual3D template, Point3D startPos, Point3D endPos)
        {
            return new PipeVisual3D()
            {
                Diameter = template.Diameter,
                InnerDiameter = template.InnerDiameter,
                Fill = template.Fill,
                ThetaDiv = template.ThetaDiv,
                Point1 = startPos,
                Point2 = endPos,
                Visible = true
            };
        }

        private static TruncatedConeVisual3D CloneCone(TruncatedConeVisual3D template, Point3D startPos, Point3D endPos)
        {
            return new TruncatedConeVisual3D()
            {
                BaseCap = template.BaseCap,
                TopCap = template.TopCap,
                TopRadius = template.TopRadius,
                BaseRadius = template.BaseRadius,
                Origin = startPos,
                Normal = startPos - endPos,
                Fill = template.Fill,
                ThetaDiv = template.ThetaDiv,
                Height = (startPos - endPos).Length,
                Visible = true
            };
        }

        private static TruncatedConeVisual3D CloneCone(TruncatedConeVisual3D template, Point3D origin, float fov)
        {
            return new TruncatedConeVisual3D()
            {
                BaseCap = template.BaseCap,
                TopCap = template.TopCap,
                TopRadius = template.TopRadius,
                BaseRadius = template.BaseRadius,
                Origin = origin,
                Normal = new Vector3D(0, 1, 0),
                Fill = template.Fill,
                ThetaDiv = template.ThetaDiv,
                Height = CalculateConeHeight(fov, template.BaseRadius, origin),
                Visible = true
            };
        }

        private static float CalculateConeFOV(TruncatedConeVisual3D cone)
        {
            var startPos = cone.Origin;
            var endPos = Point3D.Add(startPos, new Vector3D(cone.Height, 0, 0));
            var fovLen = cone.BaseRadius;
            var fovPt = Point3D.Add(endPos, new Vector3D(0, 0, fovLen));
            var hyp = (fovPt - startPos).Length;
            var adj = (endPos - startPos).Length;
            var adjOverHyp = adj / hyp;
            var rad = (float)Math.Acos(Math.Round(adjOverHyp, 2));
            return (float)Math.Max(1, Math.Round(rad * (180f / Math.PI), 2));
        }

        private static float CalculateConeHeight(float fov, double radius, Point3D origin)
        {
            var rad = Math.PI * (fov / 180);
            var opp = radius;
            var adj = (float)Math.Round(opp / Math.Tan(rad), 2);
            return adj;
        }

        #endregion

        #endregion
    }
}
